Control Flow

Pattern-Based Branching

Beyond testing simple boolean conditions with if, Rust provides pattern-based branching constructs—match, if let, let ... else, while let, and the matches! macro—that simultaneously test the shape of a value and unpack its inner fields into local variables.

Overview of Pattern-Branching Constructs

Construct Exhaustive? Best Used When
match value { ... } Yes (compiler forces handling every possible case) Branching across 2 or more variants, ranges, or structural patterns
if let PAT = expr { ... } No (handles 1 target pattern; optional else) Acting on a single pattern (such as Some(x)) and ignoring the rest
let PAT = expr else { diverge }; No (the else block must diverge: return, break, continue, panic!) Unwrapping a pattern into the current scope with a flat early-return guard
while let PAT = expr { ... } No (loops as long as expr matches PAT) Repeatedly pulling items from an iterator, stack (.pop()), or channel
matches!(expr, PAT) No (returns true or false as a bool) Testing whether a value matches a pattern inside a boolean condition or .filter()

Exhaustive Multi-Way Branching with match

A match expression compares a value against a top-to-bottom sequence of arms (PATTERN => EXPRESSION) and executes the first arm whose pattern matches:

fn describe_point(point: (i32, i32)) -> &'static str {
    match point {
        (0, 0) => "At the origin",
        (x, 0) if x > 0 => "On the positive X-axis",
        (_, 0) => "On the negative X-axis",
        (0, _) => "On the Y-axis",
        (x, y) if x == y => "On the main diagonal (x == y)",
        _ => "In a general quadrant",
    }
}

fn main() {
    for pt in [(0, 0), (5, 0), (0, -3), (4, 4), (2, 7)] {
        println!("{pt:?} -> {}", describe_point(pt));
    }
}
(0, 0) -> At the origin
(5, 0) -> On the positive X-axis
(0, -3) -> On the Y-axis
(4, 4) -> On the main diagonal (x == y)
(2, 7) -> In a general quadrant

Rich Pattern Capabilities in match

  1. Literal and Range Patterns (1..=5): Match exact numbers, characters, or inclusive ranges.
  2. Or-Patterns (|): Match any of several patterns in a single arm ('a' | 'e' | 'i' | 'o' | 'u' => "vowel").
  3. Destructuring Patterns: Unpack tuples (a, b), arrays [first, .., last], structs Point { x, y }, and enum payloads Some(v).
  4. Match Guards (if condition): Add an extra boolean filter after a pattern (Some(n) if n % 2 == 0 => ...).
  5. @ Bindings (id @ 100..=199): Test a value against a sub-pattern or range while simultaneously binding the matched value to a variable id!
  6. Wildcard (_) and Catch-All Identifier (other): Ensures the match is exhaustive (E0004 if any possible value is left uncovered).
fn inspect_code(code: u16) {
    match code {
        200 | 204 => println!("Standard success ({code})"),
        redirect @ 300..=399 => println!("Redirect code bound via @: {redirect}"),
        other => println!("Other code: {other}"),
    }
}

fn main() {
    inspect_code(200);
    inspect_code(301);
    inspect_code(500);
}
Standard success (200)
Redirect code bound via @: 301
Other code: 500

Single-Pattern Branching with if let

When you only care about one specific pattern and want to ignore all other cases, a full match with _ => {} is verbose. if let PATTERN = EXPRESSION { ... } runs its block only when the value matches PATTERN:

fn main() {
    let config_port: Option<u16> = Some(8080);

    // Unpacks `p` only if `config_port` is `Some(p)`:
    if let Some(p) = config_port {
        println!("Listening on configured port {p}");
    } else {
        println!("Using fallback port 3000");
    }
}
Listening on configured port 8080

Flat Unwrapping with let ... else

Introduced in Rust 1.65, a let ... else statement binds pattern variables directly in the current outer scope (avoiding right-ward indentation drift), and requires an else { ... } block that diverges (return, break, continue, or panic!) if the pattern does not match:

if let (Indents Happy Path):              let ... else (Keeps Happy Path Flat!):
if let Some(val) = opt {                  let Some(val) = opt else {
    // `val` only in scope inside `{}`        return; // Must diverge!
    do_work(val);                         };
}                                         // `val` is now in scope for the rest of the function!
                                          do_work(val);
fn parse_header_pair(line: &str) -> Option<(&str, u32)> {
    // 1. Unpack `Some((key, raw_val))` into the function scope, or return `None` early:
    let Some((key, raw_val)) = line.split_once(':') else {
        return None;
    };

    // 2. Unpack `Ok(number)` into the function scope, or return `None` early:
    let Ok(number) = raw_val.trim().parse::<u32>() else {
        return None;
    };

    Some((key.trim(), number))
}

fn main() {
    println!("{:?}", parse_header_pair("Content-Length: 512"));
    println!("{:?}", parse_header_pair("MalformedHeader"));
}
Some(("Content-Length", 512))
None

Conditional Pattern Loops with while let

while let PATTERN = EXPRESSION { ... } repeatedly evaluates EXPRESSION and runs the loop body for as long as the result matches PATTERN:

fn main() {
    let mut stack = vec![10, 20, 30];

    // `stack.pop()` returns `Some(top)` until the vector is empty (`None`):
    while let Some(top) = stack.pop() {
        println!("Popped: {top}");
    }
}
Popped: 30
Popped: 20
Popped: 10

Testing Patterns as a bool with matches!

The standard library macro matches!(expression, pattern) evaluates to true if expression matches pattern (including | alternatives and if guards) and false otherwise:

fn main() {
    let ch = '7';
    let is_hex_char = matches!(ch, '0'..='9' | 'a'..='f' | 'A'..='F');

    let readings = [Some(5), None, Some(15), Some(-2)];
    let positive_count = readings
        .iter()
        .filter(|&&item| matches!(item, Some(n) if n > 0))
        .count();

    println!("'{ch}' is hex: {is_hex_char}, positive readings: {positive_count}");
}
'7' is hex: true, positive readings: 2