What is a pattern guard?

A pattern guard is an if condition attached to a match arm. It lets you accept a pattern only when an additional boolean expression is true.

match value {
    pattern if condition => { /* ... */ }
    _ => { /* fallback */ }
}

The pattern is checked first. If it matches, Rust evaluates the guard. Only if the guard returns true does the arm execute.

This is especially useful when:

  • the pattern alone is too broad
  • you need to compare captured values
  • you want to keep branching logic inside match
  • you want to avoid nested if blocks after matching

A simple example

Suppose you want to classify integers by sign and parity.

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

Here, x captures the matched value, and the guard checks a property of it. This is often cleaner than matching on ranges and then adding extra logic inside each arm.

Why this is better than nested if

Without guards, you might write:

fn describe(n: i32) -> &'static str {
    match n {
        0 => "zero",
        x => {
            if x < 0 {
                "negative"
            } else if x % 2 == 0 {
                "positive even"
            } else {
                "positive odd"
            }
        }
    }
}

That works, but it hides the matching intent. Pattern guards keep the decision structure visible at the match level.


How guards interact with pattern matching

A guard does not change the pattern itself. It only adds a condition after the pattern has matched.

Consider this example:

let point = (3, -2);

match point {
    (x, y) if x == y => println!("on the diagonal"),
    (x, y) if x > 0 && y > 0 => println!("in the first quadrant"),
    _ => println!("something else"),
}

The tuple pattern (x, y) matches any two-element tuple. The guard then decides which arm is valid.

Important detail: guards are evaluated after binding

Bindings created by the pattern are available inside the guard:

match Some(42) {
    Some(n) if n > 40 => println!("large enough"),
    Some(n) => println!("small value: {n}"),
    None => println!("no value"),
}

This makes guards useful for value-based filtering while still preserving the structure of the match.


Common real-world use cases

1. Filtering enum variants by content

Pattern guards are often used with enums that carry data.

enum Event {
    Message { user: String, text: String },
    Disconnect { user: String },
}

fn handle(event: Event) {
    match event {
        Event::Message { user, text } if text.starts_with("!admin") => {
            println!("admin command from {user}");
        }
        Event::Message { user, text } => {
            println!("{user}: {text}");
        }
        Event::Disconnect { user } => {
            println!("{user} disconnected");
        }
    }
}

This keeps the command-specific logic close to the Message variant without duplicating the variant pattern.

2. Matching ranges with extra constraints

Rust supports range patterns, but sometimes a range is not enough.

fn classify(score: u8) -> &'static str {
    match score {
        0..=49 => "fail",
        50..=79 if score >= 60 => "pass",
        50..=79 => "borderline",
        80..=100 => "excellent",
        _ => "invalid",
    }
}

This example is artificial, but it shows how guards can refine a range match when business rules are more specific than the type system alone.

3. Matching Option or Result with conditions

You may want to handle only some successful values.

fn process(result: Result<String, String>) {
    match result {
        Ok(text) if !text.is_empty() => println!("got text: {text}"),
        Ok(_) => println!("empty success"),
        Err(err) => eprintln!("error: {err}"),
    }
}

This avoids extracting the Ok value and then checking it in a separate if.


Guard syntax and readability

A guard uses the if keyword directly after the pattern:

pattern if condition => expression

You can also use blocks:

match value {
    x if x > 10 => {
        println!("large");
    }
    _ => {
        println!("small or equal");
    }
}

Best practices for readability

Pattern guards are powerful, but they can become hard to scan if overused. Keep these guidelines in mind:

  • Prefer simple boolean expressions in guards.
  • Avoid heavy computation inside guards.
  • If the condition is complex, extract it into a helper function.
  • Use guards to refine a match, not to replace all logic.

A good rule of thumb: if the guard reads like a short predicate, it probably belongs there. If it reads like a mini-program, move it out.


Guards versus if let

if let is useful when you only care about one pattern. Pattern guards are better when you need multiple branches and extra conditions.

ConstructBest forExample use
if letOne pattern, optional fallbackHandle Some(x) only
match with guardsMultiple patterns with conditionsClassify values with several branches
Plain matchStructural branching onlyDifferent enum variants

Example comparison

Using if let:

if let Some(n) = maybe_number {
    if n > 10 {
        println!("large number");
    }
}

Using match with a guard:

match maybe_number {
    Some(n) if n > 10 => println!("large number"),
    _ => {}
}

The match version is often clearer when there are several outcomes, not just one.


Guards with multiple patterns

You can combine multiple patterns with | and apply one guard to the whole arm.

match ch {
    'a' | 'e' | 'i' | 'o' | 'u' if is_uppercase_context() => {
        println!("uppercase vowel in context");
    }
    'a' | 'e' | 'i' | 'o' | 'u' => {
        println!("vowel");
    }
    _ => {
        println!("other");
    }
}

The guard applies after any of the listed patterns match. This is useful when several patterns share the same additional condition.

Be careful with precedence

The guard belongs to the entire arm, not just the last pattern. If you need different conditions for different patterns, split them into separate arms.


Borrowing, ownership, and guards

Pattern guards can interact with borrowed values, but they do not move values by themselves. That said, the pattern used in the arm may move or borrow depending on how you write it.

let items = vec![String::from("alpha"), String::from("beta")];

match items.first() {
    Some(s) if s.len() > 3 => println!("long enough: {s}"),
    Some(s) => println!("short: {s}"),
    None => println!("empty"),
}

Here, first() returns Option<&String>, so the guard works with a reference. This is common in Rust code because it avoids unnecessary cloning.

Practical advice

  • Prefer matching on references when you only need to inspect data.
  • Use guards to inspect borrowed fields without taking ownership.
  • If a guard needs expensive derived data repeatedly, compute it before the match.

When not to use a pattern guard

Pattern guards are not always the best choice.

Avoid them when:

  • the condition is unrelated to the pattern
  • the logic is too complex to fit in a short predicate
  • the same guard is repeated across many arms
  • the code becomes harder to test or reason about

For example, this is probably too much for a guard:

match request {
    Request::Submit(data) if validate(&data) && rate_limit_ok() && user_is_allowed() => {
        // ...
    }
    _ => {}
}

This may be better as:

match request {
    Request::Submit(data) => {
        if validate(&data) && rate_limit_ok() && user_is_allowed() {
            // ...
        }
    }
    _ => {}
}

The second version separates structural matching from policy checks, which can improve maintainability.


A practical design pattern: classify before acting

A common Rust style is to use match with guards to classify data, then execute a focused action in each branch.

enum HttpStatus {
    Ok(u16),
    Redirect(u16),
    ClientError(u16),
    ServerError(u16),
}

fn log_status(status: HttpStatus) {
    match status {
        HttpStatus::Ok(code) if code == 200 => println!("standard success"),
        HttpStatus::Ok(code) => println!("other success: {code}"),
        HttpStatus::Redirect(code) => println!("redirect: {code}"),
        HttpStatus::ClientError(code) if code == 404 => println!("not found"),
        HttpStatus::ClientError(code) => println!("client error: {code}"),
        HttpStatus::ServerError(code) => println!("server error: {code}"),
    }
}

This style is especially useful in logging, validation, parsing, and protocol handling, where a variant alone is not enough to determine the response.


Testing guard-heavy code

Because guards encode business rules, they deserve tests. A good test suite should cover:

  • each pattern branch
  • guard-true and guard-false cases
  • boundary values for numeric conditions
  • empty, missing, and malformed inputs

For example, if a guard checks n > 10, test 10, 11, and a value that matches the pattern but fails the guard. This ensures the fallback arm behaves correctly.


Summary

Pattern guards let you write precise, expressive match statements by combining structural matching with boolean conditions. They are ideal when you want to keep branching logic readable while avoiding nested if blocks.

Use them to:

  • refine enum and tuple matches
  • filter values by runtime conditions
  • keep classification logic centralized
  • improve clarity in parsing, validation, and event handling

Use them sparingly when the condition is simple, and extract helper functions when the guard becomes too complex.

Learn more with useful resources