Skip to main content

Rust's Match Expression: Complete Guide

Published: November 25, 2025 Updated: August 28, 2026 Larry Qu 9 min read

Rust’s match expression is more powerful than switch statements in most languages. It’s exhaustive (compiler-verified), expression-based (returns a value), and handles destructuring, guards, binding, and ranges in a unified syntax. Mastering match is key to idiomatic Rust.

Basic Syntax

match compares a value against a series of patterns. The first matching pattern executes its arm. Every arm must return the same type (or ! for diverging):

fn describe(n: i32) -> &'static str {
    match n {
        0          => "zero",
        1          => "one",
        2 | 3      => "two or three",      // multiple values
        4..=9      => "four through nine", // inclusive range
        10..=99    => "two digits",
        _          => "large or negative", // catch-all
    }
}

fn main() {
    println!("{}", describe(0));   // zero
    println!("{}", describe(3));   // two or three
    println!("{}", describe(7));   // four through nine
    println!("{}", describe(100)); // large or negative
}

match is an expression — it returns the value of the matching arm:

let grade = match score {
    90..=100 => 'A',
    80..=89  => 'B',
    70..=79  => 'C',
    60..=69  => 'D',
    _        => 'F',
};

Exhaustiveness

The compiler requires all possible values to be covered. Missing a case is a compile error:

enum Color { Red, Green, Blue }

let color = Color::Red;

// This doesn't compile — Blue is not handled:
// match color {
//     Color::Red   => println!("red"),
//     Color::Green => println!("green"),
// }

// This compiles:
match color {
    Color::Red   => println!("red"),
    Color::Green => println!("green"),
    Color::Blue  => println!("blue"),
}

This is the key difference from switch statements. You can’t forget a case.

Destructuring Enums

The most powerful use of match — extracting data from enum variants:

#[derive(Debug)]
enum HttpResponse {
    Ok(String),
    Created { id: u64, location: String },
    BadRequest(Vec<String>),
    NotFound,
    InternalError { code: u32, message: String },
}

fn handle(response: HttpResponse) {
    match response {
        HttpResponse::Ok(body) => {
            println!("200 OK: {}", body);
        }
        HttpResponse::Created { id, location } => {
            println!("201 Created: id={}, location={}", id, location);
        }
        HttpResponse::BadRequest(errors) => {
            for e in &errors {
                println!("400 Bad Request: {}", e);
            }
        }
        HttpResponse::NotFound => {
            println!("404 Not Found");
        }
        HttpResponse::InternalError { code, message } => {
            eprintln!("500 Error {}: {}", code, message);
        }
    }
}

Destructuring Structs

Match can destructure struct fields directly:

#[derive(Debug)]
struct Point { x: i32, y: i32 }

fn classify_point(p: Point) -> &'static str {
    match p {
        Point { x: 0, y: 0 } => "origin",
        Point { x, y: 0 }    => "on x-axis",
        Point { x: 0, y }    => "on y-axis",
        Point { x, y } if x == y  => "on diagonal",
        Point { x, y } if x == -y => "on anti-diagonal",
        _ => "somewhere else",
    }
}

fn main() {
    println!("{}", classify_point(Point { x: 0, y: 0 })); // origin
    println!("{}", classify_point(Point { x: 5, y: 0 })); // on x-axis
    println!("{}", classify_point(Point { x: 3, y: 3 })); // on diagonal
}

Rename fields in destructuring with field: new_name:

struct Config { host: String, port: u16, debug: bool }

let cfg = Config { host: "localhost".to_string(), port: 8080, debug: true };

let Config { host: h, port: p, debug: _ } = cfg;
println!("{}:{}", h, p); // localhost:8080

Destructuring Tuples

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

    let description = match pair {
        (0, y)  => format!("on y-axis at {}", y),
        (x, 0)  => format!("on x-axis at {}", x),
        (x, y) if x == y  => format!("on diagonal at {}", x),
        (x, y) => format!("at ({}, {})", x, y),
    };
    println!("{}", description);

    // Tuple destructuring with ..
    let (a, b, .., z) = (1, 2, 3, 4, 5);
    println!("first={}, second={}, last={}", a, b, z);
}

Destructuring Slices

Match on the shape of a slice — very useful for recursive algorithms and parsing:

fn describe_list(v: &[i32]) -> String {
    match v {
        []          => "empty".to_string(),
        [x]         => format!("single: {}", x),
        [x, y]      => format!("pair: {} and {}", x, y),
        [first, .., last] => format!("starts with {}, ends with {}", first, last),
    }
}

fn main() {
    println!("{}", describe_list(&[]));         // empty
    println!("{}", describe_list(&[42]));        // single: 42
    println!("{}", describe_list(&[1, 2]));      // pair: 1 and 2
    println!("{}", describe_list(&[1, 2, 3, 4])); // starts with 1, ends with 4
}

Recursive list processing with slice patterns:

fn sum(v: &[i32]) -> i32 {
    match v {
        []            => 0,
        [head, tail @ ..] => head + sum(tail),
    }
}

fn main() {
    println!("{}", sum(&[1, 2, 3, 4, 5])); // 15
}

Match Guards

An if condition after the pattern provides extra filtering:

fn main() {
    let num = Some(7);

    let description = match num {
        Some(x) if x < 0   => format!("negative: {}", x),
        Some(x) if x == 0  => "zero".to_string(),
        Some(x) if x % 2 == 0 => format!("positive even: {}", x),
        Some(x) => format!("positive odd: {}", x),
        None => "none".to_string(),
    };
    println!("{}", description); // positive odd: 7

    // Guard with | (applies to all alternatives)
    let x = 4;
    let y = false;
    match x {
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"), // prints this
    }
    // The guard `if y` applies to the whole `4 | 5 | 6`
}

@ Bindings — Capture and Test Simultaneously

The @ operator creates a variable binding while also testing the value against a pattern:

fn classify_age(age: u32) -> &'static str {
    match age {
        n @ 0..=12    => "child",
        n @ 13..=17   => "teenager",
        n @ 18..=64   => "adult",
        n @ 65..=u32::MAX => "senior",
        _ => unreachable!(),
    }
}

// More useful: capture AND use the value in the arm
fn validate_id(id: u64) {
    match id {
        n @ 1..=999 => println!("Low ID: {}", n),
        n @ 1000..=9999 => println!("Medium ID: {}", n),
        n => println!("High ID: {}", n),
    }
}

// @ with destructuring
#[derive(Debug)]
enum Message {
    Hello { id: i32 },
}

fn main() {
    let msg = Message::Hello { id: 5 };

    match msg {
        Message::Hello { id: id_var @ 1..=10 } => {
            println!("Small id: {}", id_var)
        }
        Message::Hello { id } => {
            println!("Other id: {}", id)
        }
    }
}

Ignoring Values

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    // _ ignores one value
    match numbers {
        (first, _, third, _, fifth) => {
            println!("{}, {}, {}", first, third, fifth); // 2, 8, 32
        }
    }

    // .. ignores remaining fields
    struct Point3D { x: i32, y: i32, z: i32 }
    let p = Point3D { x: 1, y: 2, z: 3 };
    match p {
        Point3D { x, .. } => println!("x = {}", x), // 1
    }

    // .. in tuple — ignore middle
    match numbers {
        (first, .., last) => println!("{}{}", first, last), // 2 → 32
    }

    // Underscore prefix to suppress "unused variable" warning
    let _unused = String::from("doesn't matter");
    // _unused is bound but not used — no warning
    // _ alone is NOT bound — it's a wildcard, doesn't own the value
}

Nested Patterns

Match can destructure deeply nested structures:

#[derive(Debug)]
enum Shape {
    Circle { center: (f64, f64), radius: f64 },
    Rectangle { top_left: (f64, f64), bottom_right: (f64, f64) },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius, .. } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle {
            top_left: (x1, y1),
            bottom_right: (x2, y2),
        } => (x2 - x1).abs() * (y2 - y1).abs(),
    }
}

fn main() {
    let shapes = vec![
        Shape::Circle { center: (0.0, 0.0), radius: 5.0 },
        Shape::Rectangle { top_left: (0.0, 10.0), bottom_right: (4.0, 0.0) },
    ];

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

if let and while let

Concise single-pattern matching:

fn main() {
    // if let — match one variant
    let config = Some(3u8);
    if let Some(max) = config {
        println!("Max: {}", max);
    }

    // if let with else
    if let Some(max) = config {
        println!("Configured: {}", max);
    } else {
        println!("Using default");
    }

    // Chained if let else if
    let value: Result<i32, &str> = Ok(42);
    if let Ok(n) = value {
        println!("Ok: {}", n);
    } else if let Err(e) = value {
        println!("Err: {}", e);
    }

    // while let — drain a stack
    let mut stack = vec!["first", "second", "third"];
    while let Some(top) = stack.pop() {
        println!("Popped: {}", top);
    }
}

let else — Bind or Diverge

Rust 1.65+. If the pattern doesn’t match, the else block must diverge (return, break, continue, or panic):

fn process(input: &str) -> Result<u32, String> {
    let Ok(n) = input.trim().parse::<u32>() else {
        return Err(format!("'{}' is not a valid number", input));
    };

    // n is bound and available here
    Ok(n * 2)
}

fn main() {
    println!("{:?}", process("21"));    // Ok(42)
    println!("{:?}", process("abc"));   // Err("'abc' is not a valid number")
    println!("{:?}", process("  99 ")); // Ok(198)
}

let else is cleaner than the match { return Err... } pattern for validation.

The matches! Macro

Returns a boolean: true if the value matches the pattern:

#[derive(Debug)]
enum Status { Active, Inactive, Banned }

fn main() {
    let s = Status::Active;

    // Verbose:
    let is_usable = match s {
        Status::Active => true,
        _ => false,
    };

    // Concise:
    let is_usable = matches!(s, Status::Active);
    println!("{}", is_usable); // true

    // With guard
    let n = Some(42i32);
    println!("{}", matches!(n, Some(x) if x > 0)); // true

    // Multiple patterns
    println!("{}", matches!(s, Status::Active | Status::Inactive)); // true

    // Use in filter
    let statuses = vec![Status::Active, Status::Banned, Status::Active, Status::Inactive];
    let active_count = statuses.iter().filter(|s| matches!(s, Status::Active)).count();
    println!("Active: {}", active_count); // 2
}

Real-World Patterns

State Machine Transitions

#[derive(Debug, Clone, PartialEq)]
enum State {
    Idle,
    Running { job_id: u64, progress: f32 },
    Paused { job_id: u64, progress: f32 },
    Complete { job_id: u64 },
    Failed { job_id: u64, error: String },
}

impl State {
    fn transition(self, event: &str) -> State {
        match (self, event) {
            (State::Idle, "start") => State::Running { job_id: 1, progress: 0.0 },
            (State::Running { job_id, progress }, "pause") => State::Paused { job_id, progress },
            (State::Paused { job_id, progress }, "resume") => State::Running { job_id, progress },
            (State::Running { job_id, .. }, "complete") => State::Complete { job_id },
            (State::Running { job_id, .. }, "fail") => State::Failed {
                job_id,
                error: "unexpected failure".to_string(),
            },
            (state, event) => {
                println!("Invalid: {:?} + {}", state, event);
                state
            }
        }
    }
}

Command Dispatch

#[derive(Debug)]
enum Command {
    Get { key: String },
    Set { key: String, value: String, ttl: Option<u64> },
    Delete { key: String },
    Flush,
}

fn dispatch(cmd: Command) -> String {
    match cmd {
        Command::Get { key } => format!("GET {}", key),
        Command::Set { key, value, ttl: None } => {
            format!("SET {} = {}", key, value)
        }
        Command::Set { key, value, ttl: Some(ttl) } => {
            format!("SET {} = {} EX {}", key, value, ttl)
        }
        Command::Delete { key } => format!("DEL {}", key),
        Command::Flush => "FLUSHALL".to_string(),
    }
}

Summary

Feature Syntax Use when
Literal match 1 | 2 | 3 Fixed values
Range 1..=10 Ranges of integers/chars
Variable binding n Capture any value
Wildcard _ Ignore a value
Guard x if x > 0 Extra conditions
@ binding n @ 1..=10 Capture + test
Destructure enum Variant { field } Extract enum data
Destructure struct Struct { x, y } Extract struct fields
Destructure tuple (a, b, c) Extract tuple elements
Slice pattern [first, .., last] Match slice shape
if let if let Some(x) = opt Single-pattern match
let else let Ok(x) = r else { return; } Bind or diverge
matches! matches!(x, Pattern) Boolean pattern test

Resources

Comments

👍 Was this article helpful?