How to use match guards

Add an `if` condition after a pattern in a `match` arm to execute code only when both the pattern matches and the condition is true.

When patterns aren't enough

You are building a simple command router for a CLI tool. The input arrives as a Command enum with variants like Run, Debug, and Config. The Debug variant carries a verbosity level. You want to handle Debug commands differently depending on whether the verbosity is above a certain threshold. A plain match arm can catch Debug(level), but it cannot distinguish between Debug(1) and Debug(5) without writing separate arms for every possible number. That approach breaks quickly as your data grows.

Rust solves this with match guards. A guard is an if condition attached directly to a pattern. It lets you keep the structural matching of match while adding runtime filtering logic. The pattern checks the shape of the data. The guard checks the values inside it.

The concept: filtering matches

Think of a match expression like a sorting conveyor belt. Each arm is a bin with a specific shape cut into the side. Only objects that fit that shape drop into the bin. A match guard is a sensor mounted above the bin. The object slides into the shape, the sensor scans it, and only if the sensor approves does the object actually fall in. If the sensor rejects it, the object stays on the belt and keeps moving to the next bin.

This separation keeps your code readable. You declare what structure you expect in the pattern. You declare what value constraints matter in the guard. The compiler still verifies that every possible shape is handled. The guard just adds a conditional filter on top of that guarantee.

Minimal example

Here is the smallest working form. The pattern extracts a number. The guard checks a condition on that number.

/// Routes a numeric option into three distinct behaviors.
fn route_value(input: Option<i32>) {
    match input {
        // Match the Some shape, then filter by value threshold.
        Some(n) if n > 5 => println!("Greater than 5"),
        // Fallback for Some values that failed the guard.
        Some(n) => println!("5 or less"),
        // Handle the absence of a value entirely.
        None => println!("No value"),
    }
}

The if n > 5 clause is the guard. The arm only runs when two things are true. First, input must be Some. Second, the extracted n must be strictly greater than five. If input is Some(3), the pattern matches but the guard fails. The compiler moves to the next arm. If input is None, the first two patterns fail entirely and the third arm runs.

Keep your guards focused on value checks. The community convention is to treat guards as pure filters. If your guard starts calling external APIs or mutating global state, you are fighting the language design. Extract that logic into a helper function instead.

How the compiler evaluates it

The evaluation order is strict and predictable. Rust checks the pattern first. If the pattern fails, the guard never runs. This matters for performance and safety. You cannot reference variables that the pattern failed to bind.

When the pattern succeeds, Rust binds the variables. Those variables become available inside the guard and inside the arm body. They do not leak out of the match expression. The guard runs next. If it returns true, the arm executes. If it returns false, Rust continues to the next arm.

This evaluation model means guards cannot replace pattern matching for structural checks. You cannot write Some(n) if n.is_even() and expect it to catch None. The None variant never reaches the guard because the Some(_) pattern rejects it outright. The compiler enforces this at compile time. You will see E0004 (non-exhaustive patterns) if your guards accidentally cover all cases and you forget a fallback arm. The compiler treats guards as optional filters, not as exhaustive branches.

Realistic example

Consider a network packet parser. You receive different packet types, but you only want to process certain payloads based on metadata.

/// Represents incoming network packets with varying payloads.
enum Packet {
    Data { payload: Vec<u8>, priority: u8 },
    Control { opcode: u16, retry_count: u32 },
    Heartbeat,
}

/// Filters packets based on priority and retry thresholds.
fn handle_packet(pkt: Packet) {
    match pkt {
        // High priority data gets immediate processing.
        Packet::Data { priority, .. } if priority >= 8 => {
            println!("Processing high priority data");
        }
        // Normal priority data goes to the standard queue.
        Packet::Data { .. } => {
            println!("Queuing standard data");
        }
        // Retries only matter if we haven't exhausted attempts.
        Packet::Control { retry_count, .. } if retry_count < 3 => {
            println!("Scheduling control retry");
        }
        // Exhausted retries or other control opcodes get dropped.
        Packet::Control { .. } => {
            println!("Dropping control packet");
        }
        // Heartbeats always get acknowledged.
        Packet::Heartbeat => {
            println!("Acknowledging heartbeat");
        }
    }
}

Notice the .. syntax. It discards fields you do not need in the guard. The guard only references priority and retry_count. The compiler verifies that those fields exist in the variant. If you rename a field in the enum definition, the guard breaks at compile time. This keeps your filters tightly coupled to the data structure.

You can also combine guards with the @ binding operator. The @ operator lets you capture the entire variant while also extracting specific fields. This is useful when you need the full value for logging but only a subset for filtering.

/// Demonstrates combining @ binding with match guards.
fn log_and_filter(pkt: Packet) {
    match pkt {
        // Capture the whole variant for logging, extract priority for filtering.
        full @ Packet::Data { priority, .. } if priority >= 8 => {
            println!("High priority packet captured: {:?}", full);
        }
        _ => println!("Other packet type"),
    }
}

The convention here is to keep @ bindings explicit. Write full @ Packet::Data { priority, .. } rather than relying on implicit captures. It signals to future readers that you intentionally kept the whole value alive for the arm body.

Pitfalls and compiler traps

Match guards are powerful, but they introduce subtle traps if you treat them like regular if statements.

The first trap is assuming guards affect exhaustiveness. They do not. If you write Some(n) if n > 0 and Some(n) if n <= 0, the compiler still requires a None arm. Guards are conditional filters on top of patterns, not alternative patterns themselves. Forgetting the fallback triggers E0004.

The second trap is type mismatches in the condition. Guards must evaluate to a boolean. If you accidentally write Some(n) if n, the compiler rejects it with E0308 (mismatched types). Rust will not coerce integers or references into booleans. You must write the comparison explicitly.

The third trap is side effects. You can technically put function calls inside a guard. You should not. The compiler evaluates guards during pattern matching, which happens before the arm body runs. If your guard mutates state or panics, it breaks the mental model of match as a pure routing mechanism. The community standard is to keep guards free of side effects. Move complex logic into a boolean helper function and call that.

The fourth trap is borrowing. Variables bound in the pattern are borrowed by the guard. If the guard borrows a field mutably, you cannot borrow it again in the arm body. This triggers E0502 (cannot borrow as mutable because it is also borrowed as immutable). The fix is usually to clone the value before the match, or to restructure the arm to avoid overlapping borrows.

Trust the borrow checker here. It catches overlapping borrows in guards faster than you can trace them by hand.

Decision: match guards vs alternatives

Use match guards when you need to filter a single pattern by a runtime value check. Use match guards when the condition is a simple comparison, a method call, or a boolean expression that depends on extracted fields. Reach for nested matches when you are routing on multiple independent values and the logic branches deeply. Pick a helper function when the guard exceeds three lines or requires external state. Stick to plain patterns when the routing depends entirely on enum variants or tuple shapes.

Where to go next