Pattern Matching

Pattern matching is one of Rust’s most powerful features. The compiler guarantees exhaustive handling of all cases, making it impossible to forget an edge case.

Match Expressions

match is like a switch statement, but it’s an expression (returns a value) and the compiler enforces exhaustiveness.

enum Direction {
    North,
    South,
    East,
    West,
}

fn describe(dir: Direction) -> &'static str {
    match dir {
        Direction::North => "heading up",
        Direction::South => "heading down",
        Direction::East => "heading right",
        Direction::West => "heading left",
    }
    // If you remove a variant, this won't compile.
    // The compiler forces you to handle every case.
}

Pattern Types

Literal patterns

let x = 42;
match x {
    0 => println!("zero"),
    1..=9 => println!("single digit"),
    10 | 20 | 30 => println!("round number"),
    42 => println!("the answer"),
    _ => println!("something else"),  // _ is a wildcard
}

Destructuring structs and tuples

struct Point {
    x: f64,
    y: f64,
}

let p = Point { x: 0.0, y: 5.0 };

match p {
    Point { x: 0.0, y } => println!("on y-axis at y={y}"),
    Point { x, y: 0.0 } => println!("on x-axis at x={x}"),
    Point { x, y } => println!("at ({x}, {y})"),
}

// Tuples
let pair = (true, 42);
match pair {
    (true, n) if n > 0 => println!("positive truth: {n}"),
    (false, _) => println!("falsy"),
    _ => println!("other"),
}

Destructuring enums with data

enum Command {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    Color(u8, u8, u8),
}

fn handle(cmd: Command) {
    match cmd {
        Command::Quit => println!("quitting"),
        Command::Echo(msg) => println!("{msg}"),
        Command::Move { x, y } => println!("moving to ({x}, {y})"),
        Command::Color(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
    }
}

Guards

Add conditions to patterns with if:

let num = Some(42);

match num {
    Some(n) if n < 0 => println!("negative: {n}"),
    Some(n) if n > 100 => println!("large: {n}"),
    Some(n) => println!("normal: {n}"),
    None => println!("nothing"),
}

Gotcha: Guards don’t count toward exhaustiveness. The compiler can’t verify arbitrary boolean conditions, so you still need a catch-all pattern after guarded arms.

Bindings with @

Bind a value to a name while also testing it against a pattern:

match status_code {
    code @ 200..=299 => println!("success: {code}"),
    code @ 400..=499 => println!("client error: {code}"),
    code @ 500..=599 => println!("server error: {code}"),
    other => println!("unexpected: {other}"),
}

Nested patterns

Patterns compose deeply:

let msg: Option<Result<String, io::Error>> = Some(Ok("hello".into()));

match msg {
    Some(Ok(text)) => println!("got: {text}"),
    Some(Err(e)) => println!("error: {e}"),
    None => println!("nothing"),
}

Option and Result Matching

The most common use of pattern matching:

fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some("Alice".into()) } else { None }
}

// Full match
match find_user(1) {
    Some(name) => println!("Found: {name}"),
    None => println!("Not found"),
}

// Result matching with error handling
match std::fs::read_to_string("config.toml") {
    Ok(content) => process_config(&content),
    Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
        println!("No config file, using defaults");
        use_defaults();
    }
    Err(e) => return Err(e.into()),
}

if let and while let

When you only care about one variant:

// Instead of matching all cases
if let Some(name) = find_user(1) {
    println!("Found: {name}");
}

// Process items from a channel/iterator
while let Some(msg) = rx.recv().await {
    println!("Got: {msg}");
}

// With else
if let Ok(port) = env::var("PORT") {
    println!("Using port {port}");
} else {
    println!("Using default port 8080");
}

Let Chains (Rust 1.88+)

Chain multiple let patterns and conditions together. Replaces deeply nested if let blocks.

// Before: nested if let
if let Some(user) = get_user(id) {
    if let Some(email) = user.email {
        if email.ends_with("@company.com") {
            send_internal_notification(&email);
        }
    }
}

// After: let chains
if let Some(user) = get_user(id)
    && let Some(email) = user.email
    && email.ends_with("@company.com")
{
    send_internal_notification(&email);
}

Tip: Let chains work in while let too. They flatten what used to be deeply nested conditional extraction into readable linear code.

Enum-Based State Machines

Pattern matching shines for state machines. The compiler ensures every state transition is handled.

enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32, max_attempts: u32 },
    Connected { session_id: String },
    Error { message: String, retryable: bool },
}

fn next_state(state: ConnectionState) -> ConnectionState {
    match state {
        ConnectionState::Disconnected => {
            ConnectionState::Connecting { attempt: 1, max_attempts: 5 }
        }

        ConnectionState::Connecting { attempt, max_attempts } if attempt >= max_attempts => {
            ConnectionState::Error {
                message: format!("failed after {max_attempts} attempts"),
                retryable: false,
            }
        }

        ConnectionState::Connecting { attempt, max_attempts } => {
            // Try to connect...
            match try_connect() {
                Ok(id) => ConnectionState::Connected { session_id: id },
                Err(_) => ConnectionState::Connecting {
                    attempt: attempt + 1,
                    max_attempts,
                },
            }
        }

        ConnectionState::Connected { session_id } => {
            println!("Active session: {session_id}");
            ConnectionState::Connected { session_id }
        }

        ConnectionState::Error { retryable: true, .. } => {
            ConnectionState::Disconnected // retry from start
        }

        ConnectionState::Error { message, .. } => {
            panic!("Unrecoverable error: {message}");
        }
    }
}

Adding a new variant to ConnectionState forces you to update every match that handles it. The compiler won’t let you forget.

matches! Macro

Quick boolean check against a pattern:

let value = Some(42);

// Instead of: if let Some(_) = value { true } else { false }
let has_value = matches!(value, Some(_));

// With guards
let is_positive = matches!(value, Some(n) if n > 0);

// Multiple patterns
let is_vowel = matches!(c, 'a' | 'e' | 'i' | 'o' | 'u');

Patterns Everywhere

Patterns aren’t just for match. They work in:

// Function parameters
fn print_point(&(x, y): &(f64, f64)) {
    println!("({x}, {y})");
}

// let bindings
let (first, second) = ("hello", "world");
let Point { x, y } = some_point;

// for loops
for (index, value) in vec.iter().enumerate() {
    println!("{index}: {value}");
}

// Closures
let sum: i32 = pairs.iter().map(|(a, b)| a + b).sum();

Next: Lifetimes | Async Rust