Pattern Matching (match, if let, while let)

Reviewed & published by Brayan K

Pattern matching is Rust's way of testing a value's shape and pulling data out of it at the same time — the match expression checks a value against patterns and the compiler insists every case is covered.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn exhaustive match, ranges and or-patterns, binding with @, guards, destructuring enums and tuples, and the if let / let else / while let shorthands.

What You'll Learn in This Lesson

1️⃣ match: Arms, Ranges, Or-Patterns, Bindings

A match tries each arm top to bottom and runs the first that fits. Arms can be exact values, an or-pattern with |, an inclusive range with ..=, or the wildcard _. The @ operator lets you bind the matched value while testing it. Crucially, a match must be exhaustive.

fn describe(n: i32) -> &'static str {
    // match compares a value against PATTERNS, top to bottom.
    match n {
        0 => "zero",
        1 | 2 | 3 => "small",        // | is an OR-pattern
        4..=9 => "medium",           // ..= is an inclusive RANGE
        _ => "large",                // _ is the catch-all wildcard
    }
}

fn main() {
    // match must be EXHAUSTIVE — every possible value handled.
    for n in [0, 2, 7, 42] {
        println!("{} -> {}", n, describe(n));
    }

    // Binding with @ captures the matched value while testing it.
    let code = 5;
    match code {
        x @ 1..=5 => println!("low code {}", x),
        x => println!("other code {}", x),
    }
}

Because match is an expression, each arm yields a value you can return or bind — here every arm produces a string. Drop the final _ arm and the compiler refuses to build until every i32 is covered.

2️⃣ Destructuring & Guards

Patterns can reach inside structured data. You can destructure an enum variant to pull out its fields, a tuple into named pieces, or a struct's fields by name. A guard — an if after the pattern — adds a condition the pattern syntax alone can't express.

// An enum models a value that is one of several variants.
enum Shape {
    Circle(f64),                 // tuple-like variant
    Rect { w: f64, h: f64 },     // struct-like variant
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => 3.14159 * r * r,
        Shape::Rect { w, h } => w * h,
    }
}

fn main() {
    let shapes = [Shape::Circle(2.0), Shape::Rect { w: 3.0, h: 4.0 }];
    for s in &shapes {
        println!("area = {:.2}", area(s));
    }

    // Destructure a tuple AND add a guard (an extra if condition).
    let point = (3, -2);
    match point {
        (0, 0) => println!("origin"),
        (x, y) if x == -y => println!("on anti-diagonal"),
        (x, y) => println!("point ({}, {})", x, y),
    }
}

3️⃣ if let, let else & while let

When you care about just one pattern, a full match is overkill. if let runs a block only on a match. let ... else binds variables on success and forces an early exit on failure. while let repeats as long as the pattern keeps matching.

fn main() {
    // if let runs a block only when ONE pattern matches — no need for full match.
    let maybe: Option<i32> = Some(7);
    if let Some(n) = maybe {
        println!("got {}", n);
    }

    // let ... else binds on success or DIVERGES (returns/panics) on failure.
    let text = "42";
    let Ok(parsed) = text.parse::<i32>() else {
        println!("not a number");
        return;
    };
    println!("parsed = {}", parsed);

    // while let loops as long as the pattern keeps matching.
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("popped {}", top);
    }
}

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — fill in the blanks, then run it.
fn classify(n: i32) -> &'static str {
    match n {
        0 => "zero",
        // 1) Match every value from 1 to 9 inclusive with a range.
        1___9 => "single digit",
        // 2) The catch-all wildcard for everything else.
        ___ => "big",
    }
}

fn main() {
    for n in [0, 5, 100] {
        println!("{} -> {}", n, classify(n));
    }

    let maybe = Some(3);
    // 3) Unwrap the Option only when it is Some.
    if ___ Some(x) = maybe {
        println!("inner = {}", x);
    }
}

// ✅ Expected output:
//    0 -> zero
//    5 -> single digit
//    100 -> big
//    inner = 3

Mini-Challenge: An Event Handler

Model events as an enum and dispatch them with one exhaustive match, destructuring each variant. Run it with cargo run and check the output.

// 🎯 MINI-CHALLENGE: an event handler.
//
// Define an enum Event with three variants:
//   Click { x: i32, y: i32 },  Key(char),  Quit
// Write fn handle(e: &Event) -> String that uses ONE match to return:
//   - "click at (x, y)" for a Click (destructure the struct fields)
//   - "key 'c'" for a Key (bind the char)
//   - "quit" for Quit
// Then loop over a few events and print each result.
//
// ✅ Example output:
//    click at (10, 20)
//    key 'a'
//    quit

fn main() {
    // define Event, handle, and the loop here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Patterns

PatternMeaning
1 | 2 | 3Or-pattern (any of)
4..=9Inclusive range
x @ 1..=5Bind while testing
(x, y) if x > yDestructure + guard
if let Some(n) =Single-case match
while let Some(x) =Loop while matching

🎉 Lesson Complete!

Practice quiz

What does it mean that a match must be exhaustive?

  • It must be fast
  • It must have one arm
  • Every possible value must be covered
  • It must use a guard

Answer: Every possible value must be covered. The compiler requires every possible value to be handled.

What does the _ pattern do in a match?

  • Is the catch-all wildcard
  • Matches nothing
  • Binds a variable
  • Causes an error

Answer: Is the catch-all wildcard. _ is the wildcard arm that matches anything not yet covered.

What does the pattern 4..=9 match?

  • 4 to 8
  • Only 4 and 9
  • 9 to 4
  • 4 to 9 inclusive

Answer: 4 to 9 inclusive. ..= is an inclusive range, matching 4 through 9.

What does the | symbol mean in 1 | 2 | 3?

  • Bitwise OR
  • An or-pattern matching any listed value
  • A guard
  • A range

Answer: An or-pattern matching any listed value. | is an or-pattern: it matches any of the listed values.

What does x @ 1..=5 do?

  • Tests the range and binds the value to x
  • Adds x and the range
  • Imports x
  • Nothing

Answer: Tests the range and binds the value to x. @ binds the matched value to x while testing the pattern.

What is a guard in a match arm?

  • A type annotation
  • A wildcard
  • An if condition after the pattern
  • A loop

Answer: An if condition after the pattern. A guard is an extra if condition the pattern alone cannot express.

When is if let preferred over a full match?

  • When handling many cases
  • When you care about only one pattern
  • When you need exhaustiveness
  • Never

Answer: When you care about only one pattern. if let is the idiomatic shortcut when only one pattern matters.

What must the else block of a let...else do?

  • Return Ok
  • Nothing
  • Bind a variable
  • Diverge (return, break, continue, or panic)

Answer: Diverge (return, break, continue, or panic). The else block must diverge when the pattern does not match.

What does while let Some(x) = stack.pop() do?

  • Runs once
  • Loops while the pattern keeps matching
  • Errors on None
  • Pops nothing

Answer: Loops while the pattern keeps matching. while let repeats as long as pop() keeps returning Some.

Why does a guarded arm not count toward exhaustiveness?

  • Guards are slow
  • Guards are deprecated
  • The compiler cannot reason about the guard condition
  • It always counts

Answer: The compiler cannot reason about the guard condition. Because the compiler cannot evaluate the guard, you still need a fallback arm.

Continue this course

Frequently asked questions

What does it mean that a match must be exhaustive?

Exhaustiveness means a match has to cover every possible value of the type you're matching on — the compiler checks this at build time and refuses to compile if any case is left out. For an enum, you must handle each variant; for an integer, you typically end with a _ wildcard arm to catch the rest. This is a big safety win: when you add a new enum variant later, every match that didn't account for it becomes a compile error, pointing you straight at the code that needs updating, instead of silently doing the wrong thing at runtime.

When should I use if let instead of match?

Reach for if let when you only care about a single pattern and want to ignore everything else — for example, doing something only when an Option is Some. Writing a full match with an empty _ => {} arm for that case is noisier, so if let is the idiomatic shortcut. You can add an else branch for the not-matched case. Use a full match when you need to handle several cases, when you want the compiler's exhaustiveness checking, or when each branch produces a value you'll bind to a variable.

What's the difference between let else and if let?

Both test a refutable pattern, but they're shaped for opposite goals. if let runs a block when the pattern matches and otherwise falls through, so the bound variables only exist inside that block. let else inverts this: it binds the variables in the surrounding scope when the pattern matches, and its else block must diverge — return, break, continue, or panic — when it doesn't. That makes let else perfect for the 'unwrap or bail out early' pattern, keeping the happy path flat and unindented instead of nesting it inside an if let.

What do the @, |, and ..= symbols mean in a pattern?

These are pattern operators. The pipe | is an or-pattern: 1 | 2 | 3 matches any of those values in a single arm. The ..= operator is an inclusive range: 4..=9 matches every value from 4 through 9. The @ symbol is a binding: x @ 1..=5 both tests that the value is in 1..=5 and binds it to x so you can use the actual value in the arm's body. You can also add a guard with if after the pattern for arbitrary extra conditions the pattern syntax alone can't express.