Skip to main content
Technology & EngineeringRust267 lines

Pattern Matching

Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns

Quick Summary33 lines
You are an expert in Rust pattern matching for writing expressive, exhaustive, and safe control flow using match expressions and destructuring.

## Key Points

- Prefer `match` over chains of `if/else if` when checking multiple variants of an enum — the compiler ensures exhaustiveness.
- Use `if let` when you only need to handle one variant and want to ignore the rest.
- Use `let-else` for early returns on pattern failure — it is cleaner than `if let` with an else block that returns.
- Use `matches!` for boolean checks against patterns, especially in filter closures.
- Avoid the wildcard `_` catch-all if you can enumerate all variants — you will get a compile error if a new variant is added, which is a feature.
- Destructure directly in function parameters when the structure is simple:
- **Non-exhaustive match**: Forgetting a variant causes a compile error — this is intentional and one of Rust's safety guarantees. Use `_` only when you truly want to ignore remaining cases.
- **Shadowing in patterns**: A variable name in a pattern creates a new binding, it does not compare against an existing variable. Use a guard for comparison:
- **Forgetting to handle `None` and `Err`**: The compiler forces you to handle all cases, but using `_` to discard errors silently is a logic bug.
- **Move in match arms**: Matching on an owned value moves it. If you need the value after matching, match on a reference (`match &value`).
- **Guard expressions are not part of exhaustiveness**: The compiler does not consider guards when checking exhaustiveness, so you still need a catch-all arm.

## Quick Example

```rust
fn is_vowel(c: char) -> bool {
    matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
```

```rust
let status = 404;
let is_client_error = matches!(status, 400..=499);

let option: Option<i32> = Some(42);
let has_value = matches!(option, Some(x) if x > 0);
```
skilldb get rust-skills/Pattern MatchingFull skill: 267 lines
Paste into your CLAUDE.md or agent config

Pattern Matching — Rust Programming

You are an expert in Rust pattern matching for writing expressive, exhaustive, and safe control flow using match expressions and destructuring.

Core Philosophy

Pattern matching in Rust is more than syntactic sugar for switch statements -- it is the primary mechanism for safely decomposing data. When you match on an enum, the compiler proves that every variant is handled. When you destructure a struct, you name exactly the fields you care about. This exhaustiveness guarantee transforms runtime "forgot a case" bugs into compile-time errors.

The design philosophy is that data and control flow should be tightly coupled. An enum defines the shape of data; a match expression defines the behavior for each shape. Adding a new variant to an enum immediately surfaces every match that needs updating, across the entire codebase. This is why Rust programmers prefer enums over boolean flags, stringly-typed tags, or inheritance hierarchies -- the compiler enforces completeness.

Pattern matching also encourages a functional decomposition style. Rather than mutating state through method chains, you destructure values, bind the pieces you need, and produce new values. Combined with let-else for early returns and matches! for boolean checks, patterns let you write control flow that reads declaratively while remaining rigorously checked.

Anti-Patterns

  • Wildcard catch-all on enums you control: Using _ => as the last arm means adding a new variant compiles silently instead of flagging every match that needs attention. Enumerate all variants explicitly so the compiler catches missing cases.
  • Deeply nested if let chains instead of match: Writing if let Some(x) = a { if let Ok(y) = b { ... } } obscures control flow and loses exhaustiveness checking. Flatten into a single match on a tuple of the values, or use let-else for early returns.
  • Shadowing variables accidentally in patterns: A bare name in a pattern creates a new binding -- it does not compare against an existing variable with that name. This compiles without warning and silently matches everything. Use match guards (v if v == expected) for comparisons.
  • Using matches! when you need the inner value: matches! returns a boolean and discards the destructured data. If you need the bound value, use if let or match instead of calling matches! and then extracting the value separately.
  • Ignoring Err and None variants in match arms: Writing Err(_) => {} or None => {} with an empty body silently swallows failures. If the case genuinely requires no action, add a comment explaining why; otherwise, handle or propagate the error.

Overview

Pattern matching is one of Rust's most powerful features. The match expression provides exhaustive, compiler-checked branching. Patterns appear in match, if let, while let, let bindings, function parameters, and for loops. The compiler ensures every possible case is handled, preventing missed edge cases at compile time.

Core Concepts

Basic match Expression

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

fn describe(dir: Direction) -> &'static str {
    match dir {
        Direction::North => "heading north",
        Direction::South => "heading south",
        Direction::East => "heading east",
        Direction::West => "heading west",
    }
}

Matching with Values

fn classify_age(age: u32) -> &'static str {
    match age {
        0 => "newborn",
        1..=12 => "child",
        13..=19 => "teenager",
        20..=64 => "adult",
        65.. => "senior",
    }
}

Destructuring Enums

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

fn execute(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}"),
    }
}

Destructuring Structs and Tuples

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

let point = Point { x: 3.0, y: 4.0 };

let Point { x, y } = point;
println!("x={x}, y={y}");

// Tuple destructuring
let (first, second, _) = (1, 2, 3);

if let and while let

// if let: when you only care about one variant
let config_max: Option<u32> = Some(100);
if let Some(max) = config_max {
    println!("max is {max}");
}

// while let: loop while pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
    println!("{top}");
}

let-else (Rust 1.65+)

fn process(value: Option<String>) {
    let Some(name) = value else {
        println!("no value provided");
        return;
    };
    // name is bound here as String
    println!("processing {name}");
}

Implementation Patterns

Guards

fn classify(value: i32) -> &'static str {
    match value {
        n if n < 0 => "negative",
        0 => "zero",
        n if n % 2 == 0 => "positive even",
        _ => "positive odd",
    }
}

Binding with @

fn check_age(age: u32) {
    match age {
        a @ 0..=17 => println!("minor, age {a}"),
        a @ 18..=65 => println!("working age, {a}"),
        a => println!("retired, age {a}"),
    }
}

Or Patterns

fn is_vowel(c: char) -> bool {
    matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}

Nested Destructuring

enum Expr {
    Num(f64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

fn eval(expr: &Expr) -> f64 {
    match expr {
        Expr::Num(n) => *n,
        Expr::Add(a, b) => eval(a) + eval(b),
        Expr::Mul(a, b) => eval(a) * eval(b),
    }
}

Matching References

let values = vec![1, 2, 3, 4, 5];

for value in &values {
    match value {
        &1 => println!("one"),
        &2 => println!("two"),
        other => println!("{other}"),
    }
}

// Or more commonly with dereferencing
for &value in &values {
    match value {
        1 => println!("one"),
        2 => println!("two"),
        other => println!("{other}"),
    }
}

The matches! Macro

let status = 404;
let is_client_error = matches!(status, 400..=499);

let option: Option<i32> = Some(42);
let has_value = matches!(option, Some(x) if x > 0);

Matching on Result and Option Chains

fn process_input(input: &str) -> Result<(), String> {
    match input.parse::<i32>() {
        Ok(n) if n > 0 => println!("positive: {n}"),
        Ok(0) => println!("zero"),
        Ok(n) => println!("negative: {n}"),
        Err(e) => return Err(format!("parse error: {e}")),
    }
    Ok(())
}

Best Practices

  • Prefer match over chains of if/else if when checking multiple variants of an enum — the compiler ensures exhaustiveness.
  • Use if let when you only need to handle one variant and want to ignore the rest.
  • Use let-else for early returns on pattern failure — it is cleaner than if let with an else block that returns.
  • Use matches! for boolean checks against patterns, especially in filter closures.
  • Avoid the wildcard _ catch-all if you can enumerate all variants — you will get a compile error if a new variant is added, which is a feature.
  • Destructure directly in function parameters when the structure is simple:
fn distance(&(x1, y1): &(f64, f64), &(x2, y2): &(f64, f64)) -> f64 {
    ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}

Common Pitfalls

  • Non-exhaustive match: Forgetting a variant causes a compile error — this is intentional and one of Rust's safety guarantees. Use _ only when you truly want to ignore remaining cases.
  • Shadowing in patterns: A variable name in a pattern creates a new binding, it does not compare against an existing variable. Use a guard for comparison:
let expected = 42;
match value {
    expected => {} // This binds `value` to `expected`, it does NOT compare!
}
// Instead:
match value {
    v if v == expected => {}
    _ => {}
}
  • Forgetting to handle None and Err: The compiler forces you to handle all cases, but using _ to discard errors silently is a logic bug.
  • Move in match arms: Matching on an owned value moves it. If you need the value after matching, match on a reference (match &value).
  • Guard expressions are not part of exhaustiveness: The compiler does not consider guards when checking exhaustiveness, so you still need a catch-all arm.

Install this skill directly: skilldb add rust-skills

Get CLI access →