The problem: matching and keeping

You're writing a parser. You encounter a token that matches a specific pattern, but you also need the raw value to pass downstream. Or you're handling an error enum: you want to react to a specific variant, but you also need the full struct to extract a message or log the details. Matching the pattern gives you the pieces. Keeping the whole thing usually means repeating yourself or restructuring the data.

Rust gives you a shortcut. The @ operator binds the entire matched value to a variable while you still destructure it. You get the precision of pattern matching and the convenience of a single variable holding the complete value.

The @ operator: tag and capture

Think of @ like tagging a package before you open it. You inspect the label to decide what to do. If the label matches, you grab the whole package and set it aside under a name, then you open it to get the contents. The @ symbol is the tag. It says, "This whole thing matches the pattern, and I want to call it name."

The syntax is name @ pattern. The variable on the left binds to the full value. The pattern on the right checks the structure. Both must succeed for the arm to run. The binding happens before the arm body executes, so you can use the variable alongside any destructured fields.

Minimal example

Here's the smallest case: a number, a range, and a binding.

fn main() {
    let level = 7;

    match level {
        // `n @ 1..=10` checks the range and binds the value to `n`.
        // Without `@`, you'd have to repeat `level` or restructure the match.
        n @ 1..=10 => println!("Small number: {}", n),
        _ => println!("Out of range"),
    }
}

The compiler evaluates 1..=10 against level. The value 7 falls in the range, so the pattern matches. The variable n gets bound to 7. The arm body runs, and n is available. The type of n is i32, the type of the matched value. The pattern doesn't change the type of the binding.

The binding holds the whole value, not just the matched part.

Realistic example: error handling

Error handling is where @ shines. You often need to log the full error object for debugging while extracting specific fields for user-facing messages. Without @, you'd reconstruct the error or pass the original value twice, which can trigger move errors.

#[derive(Debug)]
enum ConfigError {
    MissingKey { key: String },
    InvalidValue { key: String, value: String },
}

fn handle_error(err: ConfigError) {
    match err {
        // Bind `full_err` to the whole variant while extracting `key`.
        // This avoids reconstructing the error or passing `err` twice.
        full_err @ ConfigError::MissingKey { key } => {
            log_full_error(&full_err);
            println!("Action: Prompt user for '{}'", key);
        }
        // Bind `full_err` and extract both fields.
        full_err @ ConfigError::InvalidValue { key, value } => {
            log_full_error(&full_err);
            println!("Action: Suggest fixing '{}' (got '{}')", key, value);
        }
    }
}

fn log_full_error(err: &ConfigError) {
    // Log the complete debug representation.
    eprintln!("ERROR: {:?}", err);
}

The full_err variable holds the entire ConfigError variant. The destructured fields key and value are also available. You can pass full_err to a logging function and use key for the message. The code stays clean and avoids duplication.

Log the error, act on the key. @ lets you do both without repeating yourself.

Convention: naming the binding

The community often names the binding after the value being matched. If you match err, you write err @ Error::Kind { ... }. This shadows the original err with the bound version, which is exactly what you want. It keeps the name consistent and signals that the variable now holds the matched value.

Some developers prefer a suffix like _full or whole to make the intent explicit: err_full @ Error::Kind { ... }. Both styles are common. Pick the one that fits your codebase. The key is consistency.

The move trap and partial moves

Rust's ownership rules apply inside match arms. If you bind the whole value and also destructure a non-Copy field, moving the field invalidates the whole binding. This is a partial move. The compiler catches this with E0382 (use of moved value).

struct Data {
    val: String,
}

fn main() {
    let d = Data { val: "hello".to_string() };

    match d {
        // `whole` binds the struct. `val` binds the field.
        whole @ Data { val } => {
            // `val` is moved here.
            let _v = val;

            // `whole` is now partially moved.
            // println!("{:?}", whole); // Error E0382
        }
    }
}

The pattern matches Data. whole binds the struct. val binds the String field. The arm moves val into _v. The String is now owned by _v. The struct whole is partially moved. You can't use whole anymore. The compiler rejects any attempt to access it.

Watch for partial moves. If you move a field, the whole binding goes dark.

Solving moves with ref

You can avoid partial moves by binding a reference instead of the value. Add ref to the binding. The variable becomes a reference to the matched value. The value stays in place. No moves happen.

struct Data {
    val: String,
}

fn main() {
    let d = Data { val: "hello".to_string() };

    match d {
        // `whole` is a reference to the struct. `val` is a reference to the field.
        ref whole @ Data { ref val } => {
            // Both `whole` and `val` are borrowed. No moves.
            println!("Whole: {:?}", whole);
            println!("Val: {}", val);
        }
    }
}

The ref keyword changes the binding mode. whole is &Data. val is &String. The match borrows the value instead of moving it. You can use both bindings freely. This is the standard way to handle complex structs in match arms when you need the whole thing and the parts.

Use ref when you need to avoid moves. It keeps the value alive and lets you inspect everything.

Guards and bindings

Guards let you add extra conditions to match arms. The @ binding works with guards. The guard runs after the pattern matches and the binding is created. You can use the bound variable in the guard.

fn main() {
    let score = 85;

    match score {
        // Bind `s` and check a condition using `s`.
        s @ 80..=100 if is_bonus_eligible(s) => {
            println!("Bonus score: {}", s);
        }
        _ => println!("No bonus"),
    }
}

fn is_bonus_eligible(score: i32) -> bool {
    // Simulate a check.
    score % 5 == 0
}

The pattern 80..=100 matches 85. The binding s gets 85. The guard is_bonus_eligible(s) runs. The function returns true, so the arm executes. If the guard fails, the match continues to the next arm. The binding is only available in the guard and the arm body.

Guards and bindings combine to give you fine-grained control. You can match a structure, bind the value, and filter based on the value itself.

Decision matrix

Use @ when you need the complete matched value alongside destructured fields. This happens when you log the full error object while extracting a key, or pass the whole struct to a helper while reading a flag.

Use plain destructuring when you only care about the inner data. If you never reference the wrapper or the full struct, @ adds noise.

Use a guard clause when the match depends on a condition that doesn't require binding the value. Guards let you filter without creating extra variables.

Use @ with mut when you need to mutate the bound value. The binding captures the value; adding mut lets you change it in place.

Use ref with @ when you need to avoid moves. This is essential for non-Copy types where you want to inspect both the whole and the parts without invalidating anything.

Pick the tool that matches your intent. Bind the whole thing only when you actually need it.

Where to go next