Rustmatch Expressions

match Expressions in Rust

match is one of Rust's most powerful features. It is a pattern-matching expression that compares a value against a series of patterns and executes the code associated with the first pattern that matches. Think of it as a supercharged switch statement — but exhaustive, expression-based, and capable of destructuring complex data structures in place.

Basic match Syntax

A match expression consists of the keyword match, the value being tested (the scrutinee), and a list of arms in curly braces. Each arm has a pattern, a fat arrow =>, and code to run.

RUST
fn main() {
    let number = 3;

    match number {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        4 => println!("four"),
        5 => println!("five"),
        _ => println!("something else"), // wildcard — catches everything else
    }
    // Prints: three
}
match Must Be Exhaustive

Rust requires that a match covers every possible value of the scrutinee's type. If you miss any case, the code will not compile. This prevents an entire class of bugs where unhandled values cause unexpected behavior at runtime.

RUST
fn describe(n: u8) -> &'static str {
    match n {
        0       => "zero",
        1..=9   => "single digit",
        10..=99 => "double digit",
        _       => "three digits or more", // handles 100..=255
    }
    // Without the _ arm, the compiler would say:
    // error[E0004]: non-exhaustive patterns: u8 values 100..=255 not covered
}

fn main() {
    println!("{}", describe(0));   // zero
    println!("{}", describe(7));   // single digit
    println!("{}", describe(42));  // double digit
    println!("{}", describe(200)); // three digits or more
}
Warning
The wildcard arm `_ => ...` matches anything. Always put it last — arms are evaluated top-to-bottom and the first match wins. A wildcard as the first arm would shadow all the patterns below it.
Matching Literals

You can match on any literal value: integers, characters, booleans, and string slices.

RUST
fn main() {
    // Integer literals
    let code = 404u32;
    let message = match code {
        200 => "OK",
        301 => "Moved Permanently",
        404 => "Not Found",
        500 => "Internal Server Error",
        _   => "Unknown",
    };
    println!("{}: {}", code, message); // 404: Not Found

    // Character literals
    let ch = 'A';
    match ch {
        'a'..='z' => println!("lowercase letter"),
        'A'..='Z' => println!("uppercase letter"),
        '0'..='9' => println!("digit"),
        _         => println!("other character"),
    }

    // Boolean
    let flag = true;
    match flag {
        true  => println!("flag is set"),
        false => println!("flag is clear"),
    }
}
Matching Ranges

Use the inclusive range pattern ..= inside match arms to match a span of values at once. Exclusive .. ranges are not supported in patterns — only ..=.

RUST
fn grade(score: u32) -> &'static str {
    match score {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        0..=59   => "F",
        _        => "Invalid score",
    }
}

fn age_group(age: u32) -> &'static str {
    match age {
        0..=12  => "child",
        13..=17 => "teenager",
        18..=64 => "adult",
        65..=   => "senior", // open-ended: 65 and above
        // Note: the compiler still requires exhaustiveness
    }
}

fn main() {
    println!("{}", grade(95)); // A
    println!("{}", grade(73)); // C
    println!("{}", grade(40)); // F
}
Matching Multiple Patterns with |

The pipe | lets you combine multiple patterns into one arm. This is cleaner than repeating the same code for several values.

RUST
fn main() {
    let day = 6u8; // 1=Mon, 2=Tue, ..., 6=Sat, 7=Sun

    let kind = match day {
        1 | 2 | 3 | 4 | 5 => "weekday",
        6 | 7              => "weekend",
        _                  => "invalid",
    };
    println!("{}", kind); // weekend

    // Combine ranges and literals
    let n = 5i32;
    match n {
        0            => println!("zero"),
        1 | 2 | 3    => println!("small positive"),
        4..=10       => println!("medium positive"),
        _            => println!("large or negative"),
    }
    // medium positive
}
Destructuring in match

One of Rust's most useful match features is the ability to destructure a value while matching it — pulling out its inner fields directly in the pattern.

RUST
// Destructuring a tuple
fn classify_point(point: (i32, i32)) -> &'static str {
    match point {
        (0, 0) => "origin",
        (x, 0) => "on the x-axis",
        (0, y) => "on the y-axis",
        (x, y) => "somewhere else",
    }
}

// Destructuring a struct
struct Color { r: u8, g: u8, b: u8 }

fn describe_color(c: Color) -> &'static str {
    match c {
        Color { r: 255, g: 0,   b: 0   } => "pure red",
        Color { r: 0,   g: 255, b: 0   } => "pure green",
        Color { r: 0,   g: 0,   b: 255 } => "pure blue",
        Color { r, g, b } if r == g && g == b => "shade of grey",
        _ => "mixed color",
    }
}

fn main() {
    println!("{}", classify_point((0, 0)));   // origin
    println!("{}", classify_point((5, 0)));   // on the x-axis
    println!("{}", classify_point((3, 7)));   // somewhere else

    println!("{}", describe_color(Color { r: 255, g: 0, b: 0 }));         // pure red
    println!("{}", describe_color(Color { r: 128, g: 128, b: 128 }));     // shade of grey
}
Destructuring Enums

Pattern matching shines brightest with enums. Each variant can carry different data, and match lets you access that data in a single, readable expression.

RUST
#[derive(Debug)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius }          => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height }   => 0.5 * base * height,
        }
    }

    fn name(&self) -> &'static str {
        match self {
            Shape::Circle { .. }    => "circle",
            Shape::Rectangle { .. } => "rectangle",
            Shape::Triangle { .. }  => "triangle",
        }
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle { radius: 5.0 },
        Shape::Rectangle { width: 4.0, height: 6.0 },
        Shape::Triangle { base: 3.0, height: 8.0 },
    ];

    for shape in &shapes {
        println!("{}: area = {:.2}", shape.name(), shape.area());
    }
    // circle:    area = 78.54
    // rectangle: area = 24.00
    // triangle:  area = 12.00
}
Note
The `..` pattern inside a struct variant (`Shape::Circle { .. }`) means "ignore all fields". Use it when you want to match a variant but do not need any of its data.
Binding with @ (The @ Operator)

The @ operator in a match arm lets you bind the matched value to a name while simultaneously testing it against a pattern. Without @, you must choose between testing a range and capturing the value — you cannot do both at once.

RUST
fn describe_number(n: i32) -> String {
    match n {
        // Bind to 'small' AND verify it is in 1..=5
        small @ 1..=5   => format!("{} is a small number", small),
        // Bind to 'mid' AND verify it is in 6..=10
        mid   @ 6..=10  => format!("{} is a medium number", mid),
        other           => format!("{} is something else", other),
    }
}

fn main() {
    println!("{}", describe_number(3));   // 3 is a small number
    println!("{}", describe_number(8));   // 8 is a medium number
    println!("{}", describe_number(42));  // 42 is something else

    // @ inside a struct pattern
    #[derive(Debug)]
    struct Point { x: i32, y: i32 }

    let p = Point { x: 0, y: 7 };
    match p {
        Point { x: 0, y: n @ 1..=10 } => println!("y is {} (in range)", n),
        Point { x, y }                 => println!("x={}, y={}", x, y),
    }
    // y is 7 (in range)
}
Match Guards

A match guard is an extra if condition after a pattern. The arm only fires if the pattern matches AND the guard condition is true. Guards are useful when the pattern alone is not specific enough.

RUST
fn main() {
    let pair = (2, -2);

    match pair {
        (x, y) if x == y      => println!("equal: {} == {}", x, y),
        (x, y) if x + y == 0  => println!("opposites: {} and {}", x, y),
        (x, _) if x % 2 == 0  => println!("x is even: {}", x),
        (x, y)                 => println!("other: {}, {}", x, y),
    }
    // opposites: 2 and -2

    // Guards with multiple patterns
    let n = 4;
    match n {
        x if x < 0  => println!("{} is negative", x),
        0            => println!("zero"),
        x if x % 2 == 0 => println!("{} is a positive even number", x),
        x            => println!("{} is a positive odd number", x),
    }
    // 4 is a positive even number
}
Warning
Match guards do NOT affect exhaustiveness checking. The compiler checks patterns, not guards. Even with a guard, all patterns must still collectively cover every case — the guard just narrows which arm fires.
match as an Expression

Like if, match is an expression — it produces a value. Every arm must return the same type. This lets you assign the result of a match directly to a variable or return it from a function.

RUST
fn http_status(code: u32) -> &'static str {
    match code {
        200 => "OK",
        301 => "Moved Permanently",
        400 => "Bad Request",
        401 => "Unauthorized",
        403 => "Forbidden",
        404 => "Not Found",
        500 => "Internal Server Error",
        _   => "Unknown",
    }
}

fn main() {
    // Assign from match
    let lang = "Rust";
    let paradigm = match lang {
        "Haskell" | "Erlang" => "functional",
        "Java" | "C++"       => "object-oriented",
        "Rust" | "C"         => "systems",
        _                    => "general purpose",
    };
    println!("{} is a {} language.", lang, paradigm);
    // Rust is a systems language.

    // Use match inline
    let code = 404u32;
    println!("Status {}: {}", code, http_status(code));
    // Status 404: Not Found
}
Matching Option

Option<T> is the Rust type for nullable values. It has two variants: Some(T) holding a value, and None for absence. match is the idiomatic way to handle both cases safely, with no risk of a null pointer exception.

RUST
fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some(String::from("Alice")),
        2 => Some(String::from("Bob")),
        _ => None,
    }
}

fn greet_user(id: u32) {
    match find_user(id) {
        Some(name) => println!("Hello, {}!", name),
        None       => println!("User {} not found.", id),
    }
}

fn main() {
    greet_user(1); // Hello, Alice!
    greet_user(2); // Hello, Bob!
    greet_user(9); // User 9 not found.

    // You can nest Some patterns to drill into optional values
    let maybe_number: Option<Option<i32>> = Some(Some(42));
    match maybe_number {
        Some(Some(n)) => println!("Got {}", n), // Got 42
        Some(None)    => println!("Outer Some, inner None"),
        None          => println!("Nothing at all"),
    }
}
Matching Result

Result<T, E> is Rust's type for operations that can fail. It has variants Ok(T) for success and Err(E) for failure. match on Result forces you to handle both outcomes explicitly.

RUST
use std::num::ParseIntError;

fn parse_age(s: &str) -> Result<u32, ParseIntError> {
    s.trim().parse::<u32>()
}

fn main() {
    let inputs = vec!["25", "  30  ", "abc", "-5", "42"];

    for input in inputs {
        match parse_age(input) {
            Ok(age) if age > 120 => println!("{:?} => implausibly old: {}", input, age),
            Ok(age)              => println!("{:?} => valid age: {}", input, age),
            Err(e)               => println!("{:?} => parse error: {}", input, e),
        }
    }
    // "25"    => valid age: 25
    // "  30  " => valid age: 30
    // "abc"   => parse error: invalid digit found in string
    // "-5"    => parse error: invalid digit found in string
    // "42"    => valid age: 42
}
Tip
Combining `match` on `Result` with match guards (like `if age > 120`) lets you handle different success cases separately — a pattern that would require nested ifs in other languages.
Nested Patterns

Patterns can be nested arbitrarily deep. This lets you match on the structure of complex data in a single readable expression.

RUST
#[derive(Debug)]
enum Command {
    Move  { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    Quit,
}

fn process(cmd: Command) {
    match cmd {
        Command::Quit =>
            println!("Quitting"),

        Command::Move { x, y } =>
            println!("Moving to ({}, {})", x, y),

        Command::Write(text) =>
            println!("Writing: {}", text),

        // Destructure the tuple inside the variant
        Command::ChangeColor(r, g, b) =>
            println!("Color: rgb({}, {}, {})", r, g, b),
    }
}

fn main() {
    process(Command::Move { x: 10, y: 20 });
    process(Command::Write(String::from("hello")));
    process(Command::ChangeColor(255, 128, 0));
    process(Command::Quit);
}
Why Exhaustive Matching Prevents Bugs

Consider what happens in other languages when you add a new enum variant: every switch/if-else that handles that enum silently ignores the new variant, which may cause undefined behavior or incorrect results.

In Rust, adding a new variant to an enum breaks every match on that enum with a compile error, forcing you to handle the new case everywhere. This is called exhaustiveness checking and it is one of Rust's most impactful correctness guarantees.

RUST
// Suppose we add a new variant to this enum:
#[derive(Debug)]
enum Direction {
    North,
    South,
    East,
    West,
    // New: NorthEast,
}

fn describe(d: &Direction) -> &'static str {
    match d {
        Direction::North => "heading north",
        Direction::South => "heading south",
        Direction::East  => "heading east",
        Direction::West  => "heading west",
        // If NorthEast is added, the compiler immediately reports:
        // error[E0004]: non-exhaustive patterns: Direction::NorthEast not covered
        // This forces every match to be updated — no silent bug.
    }
}

fn main() {
    println!("{}", describe(&Direction::North));
}
Note
If you intentionally want to ignore future variants (for a library where you do not control the enum), annotate the enum with `#[non_exhaustive]`. Consumers must then include a wildcard arm.
match vs if-else Chains

Aspect

match

if-else

Exhaustiveness check

Yes — compiler enforces all cases

No — easy to forget a case

Pattern destructuring

Yes — built in

No — must do separately

Range matching

Yes — 1..=10 in patterns

Possible but verbose

Multiple values per arm

Yes — with |

Possible but verbose

Match guards

Yes — if condition after pattern

Equivalent to if-else

Returns a value

Yes — expression

Yes — expression

Works with enums

Ideal

Works but misses structure

Best for

Single value, many cases, enums

Complex conditions, multiple variables

Success
You now understand Rust's match expression in depth. Key takeaways: match is exhaustive (the compiler ensures every case is handled), it can destructure tuples, structs, and enums in place, the @ operator binds a value while also testing it, match guards add extra conditions, and match is an expression that returns a value. Exhaustive matching is one of Rust's strongest tools for writing correct programs.