| |

Rust 67 🦀 Advanced Pattern Matching: Guards and @ Bindings

⏱️ Estimated read: 12 min | |

Pattern matching in Rust is more than a switch statement. Two features elevate it from simple value comparison to a concise expression of complex conditions: match guards and @ bindings. A guard is an if condition attached to a pattern that must also be true for the arm to match . An @ binding lets you test a value against a pattern while simultaneously binding the matched value to a name for use in the arm body . Together, they let you express rules that would otherwise require nested if statements or redundant reconstruction of values.

The reason these features matter is that patterns alone are structural. They can match a range, a variant, or a shape, but they cannot call a method, check a computed property, or express a condition that depends on something outside the pattern. A guard fills that gap. And a pattern alone can test a value without preserving it—if you match 3..=7, you know the value is in that range, but you do not have the value itself. An @ binding fills that gap . Neither feature is exotic; both appear in ordinary Rust code once you know to reach for them.

Key point: A match guard is an if condition after a pattern that must evaluate to true for the arm to match; an @ binding (name @ pattern) tests a value against a pattern and binds the matched value to name in the same step.


Why guards and @ bindings exist

The expressiveness gap. A pattern can say “this is a Some containing a number from 1 to 12.” It cannot say “this is a Some containing a number that is less than the value of split,” where split is a variable in scope . That is a guard’s job. The guard runs after the pattern matches structurally, and if it fails, the match continues to the next arm .

The binding gap. A pattern like 3..=7 matches a range but discards the matched value. If the arm body needs to print the actual number, you would have to match a variable (n) and then re-check the range inside the arm. That re-check does not contribute to exhaustiveness checking, and it duplicates the range . The @ binding solves both problems: n @ 3..=7 tests the range and binds the value in one step.

The exhaustiveness problem with guards. A guard is not part of the pattern for exhaustiveness purposes. The compiler cannot prove that a guarded arm covers all cases, because the guard might fail. This is why the n @ 3..=7 form is often preferable to n if (3..=7).contains(&n): the range pattern contributes to exhaustiveness, while the guard does not . The compiler will still require a fallback arm.

The | precedence problem. When a guard is combined with an or-pattern, the guard applies to the entire pattern, not just the last alternative. 4 | 5 | 6 if y means “if the value is 4, 5, or 6, and y is true” . This is a common source of confusion, and it is a reason to use parentheses if the intent differs.


a. Match guards: conditions on patterns

A match guard is written as pattern if condition => expression. The condition is a bool expression that can use any variables bound by the pattern .

fn main() {
    let num = Some(4);
    let split = 5;

    match num {
        Some(x) if x < split => println!("less than five: {}", x),
        Some(x) => println!("{}", x),
        None => (),
    }
}

The first arm matches structurally as Some(x), then the guard checks x < split. If the guard fails, the match proceeds to the second arm, which matches any Some. The guard can reference variables bound in the pattern (x) and variables from the surrounding scope (split).

Guards are only available in match expressions, not in if let or while let. This is a deliberate limitation: if let already has an if for its condition, and adding a guard syntax would be redundant. If you need guard-like behavior in an if let, you can use the if clause directly: if let Some(x) = num && x < 5.

The guard is evaluated after the pattern matches, but before the value is moved into the arm’s bindings. The Rust Reference notes that a shared reference is taken to the matched part while evaluating the guard, and the value is moved only if the guard succeeds . This means you can use shared borrows in a guard without moving out of the scrutinee.

A subtle interaction: if an or-pattern has a guard, and the guard has side effects, the guard may run multiple times—once for each alternative that matches structurally . This is rarely a problem in practice, but it is worth knowing if you are debugging.


b. @ bindings: testing and binding at once

The @ operator has the form name @ pattern. It tests the value against the pattern and, if the pattern matches, binds the entire matched value to name.

fn main() {
    let age = 15;

    match age {
        0 => println!("no birthday yet"),
        n @ 1..=12 => println!("child of age {}", n),
        n @ 13..=19 => println!("teen of age {}", n),
        n => println!("adult of age {}", n),
    }
}

The n @ 1..=12 arm matches any value in that range and binds it to n. Without the @, the arm 1..=12 would match but the body would have no access to the actual value . The final n arm is a catch-all binding, and it is required for exhaustiveness because the range patterns do not cover all u32 values.

The binding captures the value matched by the subpattern, not the textual pattern. In id @ 3..=7, id is the actual integer, not the string "3..=7". This is what makes it useful: the arm body can print, log, or pass the value on.

@ also works with enum variants and nested patterns. e @ Event::Click(..) binds the entire Event value while also matching the Click variant . Nested bindings are possible: outer @ OuterStruct { inner @ InnerStruct { value } } binds at multiple levels . The binding mode follows normal pattern rules—if the value is owned and not Copy, the @ binding may move it, so match on a reference if you need a borrowed binding .

A common use case is error reporting. When a value is out of range, you want to include the value in the error message. a @ 130..=u32::MAX => Err(format!("age {} is unrealistic", a)) binds the invalid value and uses it in the error . Without @, you would match the range and then have to re-extract the value, which is impossible without another match or a guard.


c. Combining guards and @ bindings

Guards and @ bindings compose. A guard can reference the name bound by @, and an @ binding can be used inside a guarded arm.

fn classify(value: Option<i32>) -> String {
    match value {
        Some(n @ 1..=5) if n % 2 == 0 => format!("small even: {}", n),
        Some(n @ 1..=5) => format!("small odd: {}", n),
        Some(n) => format!("large: {}", n),
        None => "none".to_string(),
    }
}

The first arm matches a Some containing a value in 1..=5, binds it to n, and then checks if it is even. The guard uses the bound name. If the value is 4, the first arm matches. If it is 3, the guard fails and the second arm matches. This pattern—range binding plus a guard for a finer condition—is a common way to avoid nested if statements.

The exhaustiveness rule still applies. The compiler sees the range 1..=5 as contributing to exhaustiveness, but the guard on the first arm does not. The second arm, without a guard, covers the same range. The third arm covers everything else. If the second arm were removed, the compiler would complain that the 1..=5 range is not exhaustively handled, even though the first arm’s guard might cover the even numbers .


Complete Example Session

The following session builds a small classifier that uses guards and @ bindings to route messages by ID range and parity.

// ============================================
// PART 1: ENUM DEFINITION
// ============================================
enum Message {
    Hello { id: u32 },
    Goodbye,
}

// ============================================
// PART 2: GUARD AND @ BINDING IN MATCH
// ============================================
fn route(msg: Message) -> String {
    match msg {
        // Bind the id and check if it is even
        Message::Hello { id: n @ 3..=7 } if n % 2 == 0 => {
            format!("priority even: {}", n)
        }
        // Bind the id for the odd case
        Message::Hello { id: n @ 3..=7 } => {
            format!("priority odd: {}", n)
        }
        // No binding needed for the reserved range
        Message::Hello { id: 10..=12 } => {
            String::from("reserved")
        }
        // Bind any other id
        Message::Hello { id: n } => {
            format!("normal: {}", n)
        }
        Message::Goodbye => String::from("bye"),
    }
}

// ============================================
// PART 3: TESTING THE ROUTES
// ============================================
fn main() {
    assert_eq!(route(Message::Hello { id: 4 }), "priority even: 4");
    assert_eq!(route(Message::Hello { id: 5 }), "priority odd: 5");
    assert_eq!(route(Message::Hello { id: 11 }), "reserved");
    assert_eq!(route(Message::Hello { id: 20 }), "normal: 20");
    assert_eq!(route(Message::Goodbye), "bye");
    println!("all tests passed");
}

The first two arms use n @ 3..=7 to bind the ID and a guard to distinguish even from odd. The third arm matches a range without binding, because the arm body does not need the value. The fourth arm binds any remaining ID. The Goodbye arm has no data to bind. The assertions verify that each route behaves as intended.


Quick Reference

Guard Syntax

FormExampleMeaning
Pattern + conditionSome(x) if x < 5Match Some and check condition
Guard with or-pattern4 | 5 | 6 if yGuard applies to all alternatives
Guard referencing bindingSome(n) if n % 2 == 0Use bound name in guard

@ Binding Syntax

FormExampleMeaning
Bind rangen @ 1..=12Test range and bind value
Bind variante @ Event::Click(..)Bind whole enum value
Bind nestedouter @ Some(inner @ Point { .. })Bind at multiple levels
Bind literalx @ 42Test exact value and bind

Guards vs @ Bindings

FeatureGuards@ Bindings
PurposeAdd runtime conditionTest pattern and bind value
ExhaustivenessDoes not contributeContributes via subpattern
Available in if letNoYes
Can call methodsYesNo (pattern only)

Common Patterns

GoalPattern
Range + use valuen @ 1..=12
Range + extra conditionn @ 1..=12 if n % 2 == 0
Variant + use whole valuee @ Event::Click(..)
Exact value + use valuex @ 42

Best Practices

✅ Do This:

n @ 1..=12 => println!("child of age {}", n)              // ✅ bind and test range
Some(x) if x < split => process(x)                        // ✅ guard on bound value
e @ Event::Click(..) => log(e)                            // ✅ bind whole variant

❌ Don’t Do This:

n if (1..=12).contains(&n) => println!("{}", n)           // ❌ no exhaustiveness
Some(x) => if x < split { process(x) }                    // ❌ nested if, not guard
1..=12 => println!("child")                               // ❌ value not available

Common Pitfalls

PitfallWhy It HappensFix
Guard not contributing to exhaustivenessGuards are runtime conditionsUse a range pattern plus a fallback
@ binding moves valueBinding mode follows pattern rulesMatch on a reference or use ref
Guard applies to only last or-alternativeMisreading precedenceGuard applies to entire | pattern
@ on an expressionRight side must be a patternMove logic to a guard
Guard not available in if letLanguage limitationUse if let with a separate if

Real-World Examples

1. Age classification with @

n @ 1..=12 => println!("child: {}", n)

2. Even/odd with guard

Some(n) if n % 2 == 0 => println!("even: {}", n)

3. Bind whole event

e @ Event::Click(..) => log(e)

4. Range + guard

n @ 1..=5 if n > 2 => println!("mid-range: {}", n)

5. Nested @ bindings

outer @ Some(inner @ Point { x, y }) => println!("{:?} {:?}", outer, inner)

6. Error with bound value

a @ 130..=u32::MAX => Err(format!("invalid age: {}", a))

7. Bind slice rest

whole @ [first, ..] => println!("first: {}, whole: {:?}", first, whole)

8. Guard on enum variant

Some(x) if x > 10 => process(x)

9. Bind exact value

x @ 42 => println!("the answer: {}", x)

10. Guard with or-pattern

4 | 5 | 6 if y => println!("yes")

Visual

Guard Flow

┌──────────────────────────────────────────────┐
│  match value                                 │
│         │                                    │
│         ▼                                    │
│  Pattern matches structurally?               │
│    ├─ NO → next arm                          │
│    └─ YES → evaluate guard                   │
│              │                               │
│         ┌────┴────┐                          │
│         │         │                          │
│       true      false                        │
│         │         │                          │
│         ▼         ▼                          │
│      run arm   next arm                      │
└──────────────────────────────────────────────┘

@ Binding

┌──────────────────────────────────────────────┐
│  n @ 1..=12                                  │
│         │                                    │
│         ▼                                    │
│  Test: is value in 1..=12?                   │
│    ├─ NO → arm does not match                │
│    └─ YES → bind value to n                  │
│              │                               │
│              ▼                               │
│         arm body can use n                   │
└──────────────────────────────────────────────┘

Exhaustiveness with Guards

┌──────────────────────────────────────────────┐
│  n @ 1..=5 if n % 2 == 0 => ...              │
│  n @ 1..=5 => ...                            │
│  n => ...                                    │
│                                              │
│  Guard does NOT contribute to exhaustiveness.│
│  The second arm (no guard) covers 1..=5.     │
│  The third arm covers everything else.       │
│  Without the second arm, compiler error.     │
└──────────────────────────────────────────────┘

@ vs Guard

┌──────────────────────────────────────────────┐
│  @ BINDING (structural)                      │
│  n @ 1..=12                                  │
│  Tests range, binds value                    │
│  Contributes to exhaustiveness               │
│                                              │
│  GUARD (runtime)                             │
│  Some(x) if x % 2 == 0                       │
│  Tests condition, does not bind              │
│  Does not contribute to exhaustiveness       │
└──────────────────────────────────────────────┘

Summary

ItemValue
Match guardpattern if condition
Guard availabilitymatch only, not if let
@ bindingname @ pattern
Range bindingn @ 1..=12
Guard with |Applies to entire pattern
Exhaustiveness@ contributes, guard does not
Nested @outer @ Some(inner @ ...)
Binding modeFollows normal pattern rules

Key takeaways:

  • Guards add runtime conditions to patterns. They are evaluated after the pattern matches, and if they fail, the match continues to the next arm .
  • Guards do not contribute to exhaustiveness. The compiler cannot prove a guarded arm covers all cases, so a fallback is still required .
  • @ binds and tests in one step. n @ 1..=12 checks the range and makes the value available in the arm body .
  • Guards can use @-bound names. n @ 1..=5 if n % 2 == 0 is a valid and common combination .
  • Guards apply to the whole or-pattern. 4 | 5 | 6 if y means all three alternatives are guarded, not just 6.
  • @ bindings can be nested. You can bind at multiple levels of a struct or enum pattern simultaneously .
  • Guards are not available in if let. Use the if clause directly for conditional matching in if let.

Remember: Match guards and @ bindings are the two features that make Rust’s pattern matching more than a switch. A guard lets you express a condition that the pattern alone cannot capture, at the cost of exhaustiveness checking. An @ binding lets you keep the value you matched, at the cost of a slightly longer pattern. Use @ when the arm body needs the value; use a guard when the arm body needs a condition that the pattern cannot express. And when you combine them, remember that the guard applies to the entire pattern—not just the last alternative—and that the compiler will still demand a fallback arm.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!