Skip to content
C Codeloom
Rust

Rust Pattern Matching Tutorial

Use Rust's match expression, if-let, while-let, and pattern syntax to destructure data safely with exhaustive compile-time checking.

·4 min read · By Codeloom
Beginner 9 min read

What you'll learn

  • match exhaustiveness
  • Pattern syntax
  • if-let and while-let
  • Guards and bindings
  • Refutable vs irrefutable

Prerequisites

  • Basic familiarity with Rust

What and Why

Pattern matching is how you destructure data in Rust: enums, structs, tuples, slices, references, and constants. The match expression must cover every possibility, so the compiler catches forgotten cases at compile time.

That exhaustiveness check is one of Rust’s most valuable safety features. Adding a new variant to an enum forces every match on it to be updated, eliminating an entire class of bugs.

Mental Model

A pattern describes the shape of a value. The compiler verifies that every possible value has at least one matching arm. Patterns can:

  • match a literal (1, "hello")
  • bind a name (x, name @ pattern)
  • destructure (Some(x), Point { x, y }, (a, b, _))
  • match by range (0..=9)
  • use OR (1 | 2 | 3)
  • attach a guard (x if x > 5)
enum Shape { Circle, Square, Triangle }
            |
            v
match s {           <-- compiler verifies all 3 variants covered
Circle => ...,
Square => ...,
Triangle => ...,
}
Match exhaustiveness check

Hands-on Example

enum Event {
    Click { x: i32, y: i32 },
    Key(char),
    Scroll(f64),
    Close,
}

fn handle(e: Event) -> String {
    match e {
        Event::Click { x, y } if x >= 0 && y >= 0 => format!("click at ({x},{y})"),
        Event::Click { .. } => "click off-screen".into(),
        Event::Key(c @ 'a'..='z') => format!("lowercase {c}"),
        Event::Key(c) => format!("key {c}"),
        Event::Scroll(amount) => format!("scroll {amount}"),
        Event::Close => "close".into(),
    }
}

fn main() {
    println!("{}", handle(Event::Click { x: 10, y: 20 }));
    println!("{}", handle(Event::Key('z')));

    // if-let for one-arm matching
    let maybe = Some(42);
    if let Some(n) = maybe {
        println!("got {n}");
    }

    // while-let for loops
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("{top}");
    }
}

The c @ 'a'..='z' syntax binds c while also asserting it’s a lowercase letter.

Common Pitfalls

Catch-all that masks bugs. Writing _ => unreachable!() defeats exhaustiveness. If you add an enum variant later, the compiler won’t warn you. Use it only when truly impossible.

Shadowing instead of comparing. match x { y => ... } binds y to x rather than comparing against an outer y. To match against a named constant, use const Y: i32 = 5; and match x { Y => ... } (uppercase constants are recognized as patterns).

Forgetting refs. When matching on &Some(x), x is the inner value by value. When matching on Some(ref x), x is a reference. Newer Rust uses match ergonomics to insert refs automatically; understand the rule when it surprises you.

Slice patterns are powerful but verbose: match nums.as_slice() { [first, .., last] => ..., [] => ... }. Easy to forget the empty case.

Practical Tips

  • Lead with the most specific patterns; the compiler matches top-down.
  • Use guards (if) for conditions on bound values, but keep them simple; complex guards belong in a function.
  • Prefer match over chains of if let when you have several variants.
  • matches!(value, Pattern) is a handy macro that returns a bool, perfect for filter predicates.
  • For struct patterns, use .. to ignore remaining fields and stay forward-compatible.
  • let else (Rust 1.65+) handles early-exit destructuring cleanly:
let Some(n) = parse(input) else { return Err("bad"); };

Wrap-up

Pattern matching is one of Rust’s defining features. It makes enums actually usable as the algebraic data types they are, eliminates whole classes of forgotten-case bugs, and reads like prose. Reach for match whenever you have a value with multiple possible shapes; reach for if let or let else for simple one-arm cases. Once it’s part of your reflex set, you’ll wish other languages had it.