ZetCode

Rust match expression

last modified October 25, 2026

In this article we show how to use the match expression in Rust.

The match expression compares a value against a series of patterns and runs the code of the first pattern that matches. It resembles the switch statement of other languages, but it is more powerful: the patterns can destructure enums, tuples, structs, and slices, bind parts of the value to variables, and test ranges and extra conditions.

match value {
    pattern1 => expression1,
    pattern2 => expression2,
    _ => default_expression,
}

Each pattern => expression pair is called an arm. The arms are tested from top to bottom. The patterns must be exhaustive: together they have to cover every possible value, otherwise the program does not compile. The underscore _ is a wildcard pattern that matches any value.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

Simple example

The following example matches integer literals.

main.rs
fn main() {
    for number in 1..=4 {
        match number {
            1 => println!("One"),
            2 => println!("Two"),
            3 => println!("Three"),
            _ => println!("Other"),
        }
    }
}

We go over the numbers 1 to 4 and print a word for each of them.

match number {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("Other"),
}

The value of number is compared with the literals 1, 2, and 3. For 4, none of them matches and the wildcard arm is executed. Without the _ arm, the compiler would reject the code, because an i32 has many more possible values.

$ cargo run -q
One
Two
Three
Other

Match as an expression

A match produces a value: the value of the executed arm. All arms must have the same type.

main.rs
fn main() {
    let day = 6;

    let kind = match day {
        1..=5 => "workday",
        6 | 7 => "weekend",
        _ => "invalid day",
    };

    println!("Day {day} is a {kind}");
}

We assign the result of the match to the kind variable.

1..=5 => "workday",
6 | 7 => "weekend",

The 1..=5 pattern matches a range of values, and the | operator combines several patterns in one arm.

$ cargo run -q
Day 6 is a weekend

Range patterns

Range patterns work with integers and characters. An inclusive range is written as a..=b, an exclusive one as a..b; one of the bounds can be omitted.

main.rs
fn main() {
    let temps = [-12, 0, 8, 21, 35];

    for t in temps {
        let desc = match t {
            ..0 => "freezing",
            0..=10 => "cold",
            11..=25 => "pleasant",
            _ => "hot",
        };

        println!("{t:>3} °C: {desc}");
    }

    for c in ['a', 'Q', '7', '%'] {
        let kind = match c {
            'a'..='z' => "lowercase letter",
            'A'..='Z' => "uppercase letter",
            '0'..='9' => "digit",
            _ => "other",
        };

        println!("{c}: {kind}");
    }
}

We classify temperatures and characters.

..0 => "freezing",
0..=10 => "cold",
11..=25 => "pleasant",
_ => "hot",

The ..0 pattern matches all values below zero. The ranges are tested in order and the first matching arm wins.

'a'..='z' => "lowercase letter",
'A'..='Z' => "uppercase letter",
'0'..='9' => "digit",

Character ranges are useful for simple classification of ASCII characters.

$ cargo run -q
-12 °C: freezing
  0 °C: cold
  8 °C: cold
 21 °C: pleasant
 35 °C: hot
a: lowercase letter
Q: uppercase letter
7: digit
%: other

Match guards

A match guard is an additional if condition after the pattern. The arm is chosen only when both the pattern matches and the condition is true.

main.rs
fn main() {
    let r = rand::random_range(-5..=5);

    match r {
        x if x > 0 => println!("The number {x} is positive"),
        0 => println!("The number is zero"),
        x => println!("The number {x} is negative"),
    }
}

We use the rand crate to get a random number in the range -5 to 5. The crate is added to the project with cargo add rand.

x if x > 0 => println!("The number {x} is positive"),

The x pattern matches any value and binds it to the variable x. The guard if x > 0 limits the arm to positive numbers.

0 => println!("The number is zero"),
x => println!("The number {x} is negative"),

Zero is matched by its literal. The last arm catches all remaining values, which can only be negative numbers. The compiler does not evaluate guards when it checks exhaustiveness, so a catch-all arm is needed.

$ cargo run -q
The number 2 is positive
$ cargo run -q
The number 3 is positive
$ cargo run -q
The number 4 is positive

Binding with @

The @ operator binds the matched value to a variable while testing it against a pattern.

main.rs
fn main() {
    for age in [3, 15, 42, 130] {
        match age {
            n @ 0..=12 => println!("{n}: child"),
            n @ 13..=19 => println!("{n}: teenager"),
            n @ 20..=120 => println!("{n}: adult"),
            n => println!("{n}: not a valid age"),
        }
    }
}

We classify ages and print each value in the message.

n @ 0..=12 => println!("{n}: child"),

The value is tested against the range 0..=12 and, if it matches, bound to n. A plain range pattern does not give us access to the value.

$ cargo run -q
3: child
15: teenager
42: adult
130: not a valid age

Matching enums

The match expression is the main tool for working with enums. The patterns can extract the data stored in the variants.

main.rs
enum Shape {
    Circle(f64),
    Rectangle { width: f64, height: f64 },
    Point,
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle { width, height } => width * height,
        Shape::Point => 0.0,
    }
}

fn main() {
    let shapes = [
        Shape::Circle(1.5),
        Shape::Rectangle { width: 3.0, height: 4.0 },
        Shape::Point,
    ];

    for s in &shapes {
        println!("{:.2}", area(s));
    }
}

The Shape enum has three variants: a tuple variant, a struct variant, and a unit variant.

match shape {
    Shape::Circle(r) => std::f64::consts::PI * r * r,
    Shape::Rectangle { width, height } => width * height,
    Shape::Point => 0.0,
}

Each arm destructures one variant and binds its fields to variables. The three arms cover all variants, so no wildcard is needed. If we add a new variant to the enum later, the compiler points out every match that does not handle it.

$ cargo run -q
7.07
12.00
0.00

Matching Option and Result

The Option and Result types are enums too. Matching them is the basic way to handle missing values and errors.

main.rs
fn main() {
    let words = ["sky", "forest", "cup"];

    match words.iter().position(|&w| w == "cup") {
        Some(idx) => println!("cup found at index {idx}"),
        None => println!("cup not found"),
    }

    for text in ["42", "forty-two"] {
        match text.parse::<i32>() {
            Ok(n) => println!("parsed {n}"),
            Err(e) => println!("cannot parse {text:?}: {e}"),
        }
    }
}

We search for a word and parse two strings.

match words.iter().position(|&w| w == "cup") {
    Some(idx) => println!("cup found at index {idx}"),
    None => println!("cup not found"),
}

The position method returns an Option. The Some(idx) pattern extracts the index.

match text.parse::<i32>() {
    Ok(n) => println!("parsed {n}"),
    Err(e) => println!("cannot parse {text:?}: {e}"),
}

The parse method returns a Result. We handle both the success and the error case. More on this in Rust Option type.

$ cargo run -q
cup found at index 2
parsed 42
cannot parse "forty-two": invalid digit found in string

Matching tuples

Tuple patterns test several values at once.

main.rs
fn main() {
    let points = [(0, 0), (0, 7), (4, 0), (3, 3), (2, -5)];

    for p in points {
        match p {
            (0, 0) => println!("{p:?} is the origin"),
            (0, y) => println!("{p:?} is on the y axis at {y}"),
            (x, 0) => println!("{p:?} is on the x axis at {x}"),
            (x, y) if x == y => println!("{p:?} is on the diagonal"),
            _ => println!("{p:?} is somewhere else"),
        }
    }
}

We describe the position of points in a plane.

(0, 0) => println!("{p:?} is the origin"),
(0, y) => println!("{p:?} is on the y axis at {y}"),
(x, 0) => println!("{p:?} is on the x axis at {x}"),
(x, y) if x == y => println!("{p:?} is on the diagonal"),

Literals in a tuple pattern must match exactly, while names bind the corresponding values. The order of the arms matters: the origin (0, 0) would also match the second and the third arm, so it is tested first.

$ cargo run -q
(0, 0) is the origin
(0, 7) is on the y axis at 7
(4, 0) is on the x axis at 4
(3, 3) is on the diagonal
(2, -5) is somewhere else

Matching structs

Struct patterns test and extract individual fields.

main.rs
struct User {
    name: String,
    age: u32,
    active: bool,
}

fn main() {
    let users = [
        User { name: String::from("John Doe"), age: 34, active: true },
        User { name: String::from("Roger Roe"), age: 17, active: true },
        User { name: String::from("Lucy Smith"), age: 52, active: false },
    ];

    for user in &users {
        match user {
            User { active: false, name, .. } => println!("{name}: inactive"),
            User { age: 0..18, name, .. } => println!("{name}: minor"),
            User { name, age, .. } => println!("{name}: adult, {age}"),
        }
    }
}

We sort users into three groups.

User { active: false, name, .. } => println!("{name}: inactive"),
User { age: 0..18, name, .. } => println!("{name}: minor"),

A field can be compared with a literal or a range, or bound to a variable of the same name. The .. rest pattern ignores the remaining fields. Since we match a reference, the bound variables are references to the fields.

$ cargo run -q
John Doe: adult, 34
Roger Roe: minor
Lucy Smith: inactive

Matching slices

Slice patterns match by the number of elements and can extract the first and last ones.

main.rs
fn describe(vals: &[i32]) -> String {
    match vals {
        [] => String::from("empty"),
        [x] => format!("one element: {x}"),
        [first, second] => format!("two elements: {first} and {second}"),
        [first, .., last] => format!("{} elements, from {first} to {last}", vals.len()),
    }
}

fn main() {
    println!("{}", describe(&[]));
    println!("{}", describe(&[5]));
    println!("{}", describe(&[5, 6]));
    println!("{}", describe(&[5, 6, 7, 8, 9]));
}

The describe function returns a different message depending on the length of the slice.

[] => String::from("empty"),
[x] => format!("one element: {x}"),
[first, second] => format!("two elements: {first} and {second}"),
[first, .., last] => format!("{} elements, from {first} to {last}", vals.len()),

The patterns match an empty slice, a slice of one element, of two elements, and of any other length. The .. in the last pattern stands for any number of elements in the middle.

$ cargo run -q
empty
one element: 5
two elements: 5 and 6
5 elements, from 5 to 9

The matches! macro

The matches! macro tests whether a value matches a pattern and returns a bool. It is a short form of a match with true and false arms.

main.rs
fn main() {
    let chars = ['a', 'E', 'x', 'o', '1'];

    for c in chars {
        let vowel = matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u');
        println!("{c}: {vowel}");
    }

    let vals = [3, 12, 7, 20];
    let count = vals.iter().filter(|&&v| matches!(v, 10..=20)).count();
    println!("{count} values between 10 and 20");
}

We test characters for vowels and count values in a range.

let vowel = matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u');

The macro returns true if the lowercased character is one of the listed vowels.

let count = vals.iter().filter(|&&v| matches!(v, 10..=20)).count();

The macro is handy in closures that need a condition, such as the one passed to filter.

$ cargo run -q
a: true
E: true
x: false
o: true
1: false
2 values between 10 and 20

Source

The Rust Programming Language: The match Control Flow Construct

The Rust Programming Language: Patterns and Matching

In this article we have worked with the match expression in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.