Match Guards

A match guard is an extra if condition attached to an arm of a match expression, so that arm fires only when the pattern matches and the condition is true. Patterns alone can check a value’s shape and pull it apart, but they can’t express arbitrary logic like “this number is even” or “these two bound variables are equal to each other” — that gap is exactly what match guards fill. They turn match from a purely structural tool into one that combines structure with computation, which is why they show up constantly in real Rust code for validation, classification, and business rules.

Overview: How Match Guards Work

Recall that a normal match arm looks like PATTERN => EXPRESSION. Rust tries each arm’s pattern, top to bottom, and runs the first one that matches the value’s shape. A match guard inserts a second gate after the pattern: PATTERN if CONDITION => EXPRESSION. When Rust reaches an arm, it first checks whether the pattern matches. If it does not, Rust skips straight to the next arm — the guard is never even evaluated. If the pattern does match, Rust then evaluates CONDITION as an ordinary boolean expression. If the condition is true, that arm runs. If it’s false, Rust behaves as if the whole arm never matched at all and moves on to try the next arm from scratch — it does not backtrack and try other ways of matching the same pattern.

The crucial part of the mental model is that the guard’s condition can freely use any variables the pattern just bound, plus any variables already in scope from the surrounding function. This is what makes guards so powerful: a plain pattern like (x, y) can only check that the value is a two-element tuple, but a guard on that arm, (x, y) if x == y, can check a relationship between the two bound values, something no pattern by itself can express. Guards are also evaluated lazily and only for the arm currently being tried, so if an earlier arm’s pattern matches and its guard passes, later arms (even ones with the same pattern) are never reached.

One subtlety that trips people up: the compiler’s exhaustiveness checker (the same one that requires every match to cover all possible values) treats a guarded arm as if it might not fire, even when the pattern itself is exhaustive. Because a guard’s condition is arbitrary runtime logic, the compiler cannot prove that your guards, taken together, cover every case. That means a match built entirely out of guarded arms almost always still needs a final catch-all arm, a point covered in more detail in Common Mistakes below.

Syntax

The general form of a guarded arm is:

match VALUE {
    PATTERN if CONDITION => EXPRESSION,
    PATTERN if CONDITION => EXPRESSION,
    _ => EXPRESSION,
}
  • PATTERN — any valid match pattern: a literal, a variable binding, a tuple/struct destructuring, a range like 1..=10, an @ binding, or alternatives joined with |.
  • if CONDITION — a normal bool expression, evaluated only after the pattern matches. It can reference variables bound by PATTERN as well as outer variables.
  • EXPRESSION — the code that runs when both the pattern matches and the guard is true.
  • When PATTERN uses | to list alternatives, the guard applies to all of them, not just the last one — 1 | 2 | 3 if condition means “1, 2, or 3, and then also condition”.

Examples

Example 1: A guard that checks arithmetic on the bound value

fn main() {
    let numbers = [4, 7, 12, 15, 20];

    for n in numbers {
        match n {
            x if x % 2 == 0 => println!("{x} is even"),
            x if x % 2 != 0 => println!("{x} is odd"),
            _ => unreachable!(),
        }
    }
}

Output:

4 is even
7 is odd
12 is even
15 is odd
20 is even

Here the pattern in every arm is just x, which matches any i32 and binds it to x. The pattern alone can’t tell even from odd — that logic lives entirely in the guard, x % 2 == 0. Since every integer is either even or odd, the final _ => unreachable!() arm can never actually run; it exists only to satisfy the compiler’s exhaustiveness check, and unreachable!() documents that intent by panicking with a clear message if it’s ever somehow reached.

Example 2: A guard comparing two bound variables to each other

fn main() {
    let pair = (5, -5);
    let x_threshold = 0;

    match pair {
        (x, y) if x + y == 0 => println!("These cancel out: {x} and {y}"),
        (x, _) if x > x_threshold => println!("First is positive: {x}"),
        (x, _) if x < x_threshold => println!("First is negative: {x}"),
        _ => println!("Something else"),
    }
}

Output:

These cancel out: 5 and -5

All four arms use the same shape of pattern (a two-element tuple), so the pattern itself can’t distinguish between them — the guards do all the real work. Notice that the second and third arms both use the literal pattern (x, _); that’s fine, because their guards (x > x_threshold and x < x_threshold) are mutually exclusive, and the compiler does not warn about “duplicate” patterns when guards differ, since it can’t prove they overlap. Also notice that the guard on the first arm reads x_threshold, a variable from the enclosing scope, not from the pattern — guards can mix pattern-bound and outer variables freely.

Example 3: Combining @ bindings, ranges, and guards

fn main() {
    let scores = [45, 72, 88, 95, 100];

    for score in scores {
        let grade = match score {
            s @ 90..=100 if s == 100 => "A+ (perfect!)",
            90..=100 => "A",
            80..=89 => "B",
            70..=79 => "C",
            n if n < 0 || n > 100 => "Invalid",
            _ => "F",
        };
        println!("Score {score}: {grade}");
    }
}

Output:

Score 45: F
Score 72: C
Score 88: B
Score 95: A
Score 100: A+ (perfect!)

This is a more realistic use of guards: most arms are handled by plain range patterns, but the very first arm needs something a range alone can’t express — “in 90..=100, and specifically equal to 100″. The @ operator (s @ 90..=100) binds the matched value to s while still checking it falls in the range, and the guard then narrows further to the single value 100. Arms are tried top to bottom, so the more specific guarded arm must come before the plain 90..=100 arm, or it would never be reached.

How It Works Step by Step

For a value like score = 100 going through Example 3, the compiler-generated logic effectively does the following at runtime: (1) try the pattern s @ 90..=100 against 100 — it matches, and s is bound to 100; (2) because the pattern matched, evaluate the guard s == 100 — this is true; (3) since both the pattern and guard succeeded, run that arm’s expression, "A+ (perfect!)", and skip every remaining arm entirely. For score = 95: the first pattern still matches (95 is in 90..=100) and binds s, but now the guard s == 100 is false, so Rust discards this attempt completely and moves to the next arm, 90..=100 (no guard), which matches unconditionally and runs. This step-by-step trace is why guard order matters and why a guard failing does not mean the whole pattern branch is abandoned forever — only that specific arm is skipped, and matching restarts fresh at the next arm.

Common Mistakes

Mistake 1: Assuming guards make a match exhaustive when they don’t

It’s tempting to think that if your guards logically cover every case, you can skip the wildcard arm. The compiler disagrees, because it cannot reason about arbitrary boolean expressions — it only knows patterns are exhaustive, never that a set of if conditions is.

fn classify(n: i32) -> &'static str {
    match n {
        x if x > 0 => "positive",
        x if x < 0 => "negative",
    }
}

fn main() {
    println!("{}", classify(5));
}

This fails to compile with an error along the lines of:

error[E0004]: non-exhaustive patterns: match guards are not considered
exhaustive, so a wildcard `_` arm (or additional patterns) is required

The fix is simply to add a catch-all arm, even though a careful reader can see the two guards miss only zero:

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

fn main() {
    println!("{}", classify(5));
    println!("{}", classify(-3));
    println!("{}", classify(0));
}

Output:

positive
negative
zero

Mistake 2: Comparing a reference-bound pattern variable to a plain literal

When you iterate over &some_vec, each item you get is a reference (&i32, not i32). A pattern like x then binds x as that same reference type, and comparing a reference directly to a bare integer literal in a guard is a type mismatch, not something Rust silently dereferences for you.

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

    for n in &numbers {
        match n {
            x if x == 20 => println!("found 20"),
            _ => {}
        }
    }
}

This produces a compile error similar to:

error[E0308]: mismatched types
expected `i32`, found `&i32`

The fix is to dereference the bound variable before comparing it, so both sides of == are plain i32 values:

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

    for n in &numbers {
        match n {
            x if *x == 20 => println!("found 20"),
            _ => println!("skipping {n}"),
        }
    }
}

Output:

skipping 10
found 20
skipping 30

Best Practices

  • Reach for a guard only when a pattern genuinely can’t express the condition — comparing two bound values, using arithmetic, or calling a function. For simple equality or ranges, a plain pattern (like a range pattern) is clearer and lets the compiler verify exhaustiveness for you.
  • Always keep (or add) a final _ => ... arm in any match that uses guards, even if you’re convinced the guards are logically exhaustive — the compiler cannot verify that for you.
  • Remember that a guard on a |-joined pattern applies to every alternative in that list, not just the nearest one; if you need different conditions per alternative, write separate arms instead.
  • Keep guard conditions short and readable. If a guard grows into several `&&`/`||` clauses, consider extracting it into a small named function or a local `bool` variable computed just above the `match`.
  • Avoid side effects (like mutating state or printing) inside a guard condition — guards are meant to be pure checks, and hiding side effects there makes control flow hard to follow.
  • When matching on a reference (for example, iterating with `&collection`), remember bound pattern variables are references too; dereference with `*` before comparing to owned values.

Practice Exercises

Exercise 1: Write a function classify_temp(c: f64) -> &'static str that uses match with guards to return "freezing" for below 0.0, "cold" for 0.0 up to (but not including) 15.0, "warm" for 15.0 up to (but not including) 30.0, and "hot" otherwise. Call it on -5.0, 10.0, 20.0, 35.0 and print each result.

Exercise 2: Given a tuple (i32, i32) representing a point’s (x, y) coordinates, write a match with guards that prints "Origin" when both are zero, "On the x-axis" when only y is zero, "On the y-axis" when only x is zero, and otherwise one of "Quadrant I" through "Quadrant IV" based on the signs of x and y. Test it with (0, 0), (3, 0), (0, -4), (2, 3), and (-2, -3).

Exercise 3: Given a Vec<Option<i32>>, loop over it by reference and use a guarded match to print "big positive" for Some(n) where n > 100, "small positive" for Some(n) where 0 < n <= 100, "non-positive" for any other Some(n), and "empty" for None. Hint: since you’re matching on a reference to the Option, you’ll need Some(n) to bind n as a reference, so dereference it in the guard.

Summary

  • A match guard is an if CONDITION appended to a pattern; the arm runs only when both the pattern matches and the condition is true.
  • Guards can reference variables the pattern just bound as well as variables already in the enclosing scope.
  • If a guard’s condition is false, Rust does not retry the same pattern differently — it moves on and tries the next arm from scratch.
  • The compiler cannot prove that a set of guards covers every case, so a match built from guarded arms almost always still needs a final _ catch-all, or it fails to compile.
  • A guard attached to a |-joined set of alternative patterns applies to all of them, not just the last one.
  • When matching on a reference, pattern-bound variables are references too; dereference with * before comparing to plain values in a guard.
  • Use guards for logic patterns can’t express (comparisons between bound values, arithmetic, function calls); prefer plain patterns and ranges when they’re enough, since the compiler can then verify exhaustiveness for you.