TechLead
Lesson 7 of 28
5 min read
Rust

Pattern Matching & Control Flow

Master Rust match expressions, if let, while let, destructuring, match guards, and exhaustive pattern matching for robust code.

The match Expression

Rust's match is like a supercharged switch statement. It must be exhaustive — every possible value must be handled. The compiler enforces this, which eliminates an entire class of bugs.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

#[derive(Debug)]
enum UsState {
    Alabama, Alaska, Arizona, California,
}

fn value_in_cents(coin: &Coin) -> u32 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("Quarter from {:?}", state);
            25
        }
    }
}

fn main() {
    let coin = Coin::Quarter(UsState::California);
    println!("Value: {} cents", value_in_cents(&coin));
}

Matching on Values and Ranges

fn main() {
    let number = 13;

    match number {
        1 => println!("One"),
        2 | 3 | 5 | 7 | 11 | 13 => println!("Prime"),
        14..=19 => println!("Teen (14-19)"),
        _ => println!("Something else"), // _ is the wildcard
    }

    // Matching with bindings
    let msg = match number {
        n @ 1..=12 => format!("{n} is small"),
        n @ 13..=19 => format!("{n} is a teen"),
        n => format!("{n} is big"),
    };
    println!("{msg}");

    // Match is an expression — it returns a value!
    let boolean = true;
    let binary = match boolean {
        true => 1,
        false => 0,
    };
    println!("{binary}");
}

Destructuring in Patterns

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

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

fn main() {
    // Destructuring structs
    let p = Point { x: 5, y: 10 };
    let Point { x, y } = p;
    println!("x={x}, y={y}");

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

    // Destructuring enums
    let cmd = Command::Move { x: 10, y: 20 };
    match cmd {
        Command::Quit => println!("Quit"),
        Command::Echo(msg) => println!("Echo: {msg}"),
        Command::Move { x, y } => println!("Move to ({x}, {y})"),
        Command::Color(r, g, b) => println!("Color: ({r}, {g}, {b})"),
    }

    // Destructuring tuples
    let (a, b, c) = (1, "hello", true);
    println!("{a} {b} {c}");

    // Nested destructuring
    let ((feet, inches), name) = ((5, 11), "Alice");
    println!("{name} is {feet}'{inches}"");
}

Match Guards and @ Bindings

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

    // Match guard: extra condition with 'if'
    match num {
        Some(n) if n < 0 => println!("Negative: {n}"),
        Some(n) if n == 0 => println!("Zero"),
        Some(n) if n > 100 => println!("Big: {n}"),
        Some(n) => println!("Normal: {n}"),
        None => println!("Nothing"),
    }

    // @ binding: bind a value while testing a pattern
    let age = 25;
    match age {
        n @ 0..=12 => println!("Child aged {n}"),
        n @ 13..=19 => println!("Teenager aged {n}"),
        n @ 20..=64 => println!("Adult aged {n}"),
        n @ 65.. => println!("Senior aged {n}"),
        _ => unreachable!(),
    }

    // Combining guards and bindings
    let x = Some(5);
    match x {
        Some(n @ 1..=10) if n % 2 == 0 => println!("Even 1-10: {n}"),
        Some(n @ 1..=10) => println!("Odd 1-10: {n}"),
        Some(n) => println!("Other: {n}"),
        None => println!("None"),
    }
}

if let and while let

fn main() {
    let config_max = Some(3u8);

    // Verbose: match with only one interesting case
    match config_max {
        Some(max) => println!("Max: {max}"),
        _ => (),
    }

    // Concise: if let for single-pattern matching
    if let Some(max) = config_max {
        println!("Max: {max}");
    }

    // if let with else
    let coin = Coin::Penny;
    if let Coin::Quarter(state) = &coin {
        println!("Quarter from {:?}", state);
    } else {
        println!("Not a quarter");
    }

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

    // let else: bind or diverge (Rust 1.65+)
    let s = "42";
    let Ok(n) = s.parse::() else {
        println!("Failed to parse");
        return;
    };
    println!("Parsed: {n}");
}

enum Coin { Penny, Quarter(String) }

Why match is Better Than switch

  • Exhaustive: You must handle every possible variant — no silent fallthrough
  • Destructuring: Extract data from enums, structs, and tuples inline
  • Expression-based: match returns a value you can assign
  • No fallthrough: Each arm is independent — no accidental bugs from missing break
  • Pattern guards: Add conditions to patterns for precise control

Key Takeaways

  • match must be exhaustive — the compiler catches unhandled cases
  • ✅ Patterns can destructure structs, enums, tuples, and nested structures
  • ✅ Use match guards (if) for additional conditions on patterns
  • if let and while let simplify single-pattern matching
  • @ bindings let you capture a value while testing a pattern

Continue Learning