---
title: "11.4 match in depth"
description: "The full power of match: binding patterns, multiple patterns, ranges, match guards, and the @ binding operator, plus the catch-all patterns _ and a name."
url: "https://learnrust.net/chapter-11/match-in-depth/"
last_updated: "2026-06-13"
---

# 11.4 match in depth

You met `match` for control flow in lesson [7.3](https://learnrust.net/chapter-7/introduction-to-match/) and have leaned on it for three enum lessons since. It's time to learn everything it can do, because `match` is the most powerful single construct in Rust, and the patterns you write in it are a small language of their own. This lesson is a tour of that language.

## Binding patterns: naming what you match

You've already seen the central trick: a pattern can *bind* a name to the value it matched. Lesson [11.2](https://learnrust.net/chapter-11/enums-with-data/)'s `Message::Write(text)` matched the variant and named its payload `text`. The same works for `Option`:

```rust
fn main() {
    let config: Option<i32> = Some(42);
    match config {
        Some(value) => println!("got {value}"),
        None => println!("nothing"),
    }
}
```

```
got 42
```

`Some(value)` only matches `Some`, and when it does, `value` is bound to the inner `i32`, usable on the right-hand side. The name is yours to choose; `Some(n)`, `Some(count)`, `Some(x)` all work. Binding is what makes `match` more than a fancy `if`: it checks the shape and extracts the contents in one move.

## Multiple patterns with |

One arm can match several patterns at once with `|`, read "or". You saw this for vowels in lesson [7.3](https://learnrust.net/chapter-7/introduction-to-match/):

```rust
fn main() {
    let n = 5;
    match n {
        1 | 3 | 5 | 7 | 9 => println!("odd single digit"),
        0 | 2 | 4 | 6 | 8 => println!("even single digit"),
        _ => println!("something else"),
    }
}
```

```
odd single digit
```

Each arm fires if *any* of its patterns match. This keeps related cases together instead of duplicating the same right-hand side across many arms.

## Ranges with ..=

For matching a span of values, `..=` (the inclusive range from lesson [7.5](https://learnrust.net/chapter-7/for-loops-and-ranges/)) works as a pattern:

```rust
fn grade(score: u32) -> char {
    match score {
        90..=100 => 'A',
        80..=89 => 'B',
        70..=79 => 'C',
        60..=69 => 'D',
        _ => 'F',
    }
}

fn main() {
    println!("{}", grade(85));   // B
    println!("{}", grade(42));   // F
}
```

```
B
F
```

`90..=100` matches any value from 90 through 100 inclusive. Ranges in patterns must be inclusive (`..=`) or open-ended; the half-open `..` isn't allowed here, because the compiler wants the boundaries unambiguous. Range patterns work on integers and `char`s (`'a'..='z'` matches any lowercase letter), which is often cleaner than a long `|` chain.

## Match guards: an extra condition

Sometimes matching the shape isn't enough and you need an additional test. A **match guard** is an `if` condition tacked onto an arm; the arm fires only if the pattern matches *and* the guard is true:

```rust
fn main() {
    let pair = (3, -3);
    match pair {
        (x, y) if x + y == 0 => println!("sum to zero"),
        (x, _) if x % 2 == 0 => println!("first is even"),
        _ => println!("no special property"),
    }
}
```

```
sum to zero
```

The first arm matches any pair, but only fires when `x + y == 0`. Guards let a pattern reach beyond pure structure to arbitrary logic, including comparisons between the bound values, which a plain pattern can't express. The cost is that the compiler can't reason about guards for exhaustiveness, so you'll usually still want a final catch-all.

## The two catch-alls: _ and a name

When you don't need every case spelled out, two patterns match anything. You've used `_`, the wildcard, since lesson [7.3](https://learnrust.net/chapter-7/introduction-to-match/): it matches any value and binds nothing. The other is a plain name, which matches anything *and binds it*, so you can use the value:

```rust
fn main() {
    let dice = 4;
    match dice {
        3 => println!("three: special"),
        7 => println!("seven: special"),
        other => println!("rolled a {other}"),
    }
}
```

```
rolled a 4
```

`other` catches every value 3 and 7 didn't, and names it so the arm can print it. Use `_` when you want to ignore the value, a name when you want it. Both make a `match` exhaustive, which is why a catch-all is the usual way to close a `match` on a type with too many values to list (like a full `i32`).

> **Warning**
>
> Catch-all arms must come *last*. `match` tries arms top to bottom and takes the first that matches, so a `_` or a bare name placed early would swallow everything below it. The compiler warns about unreachable arms (`unreachable_pattern`) when this happens, the same kind of dead-code warning you met in lesson [2.2](https://learnrust.net/chapter-2/return-values/). Specific patterns first, general patterns last.

## Binding and testing at once with @

Occasionally you want to test a value against a range *and* keep the value. The `@` operator binds a name to a value while also testing it against a pattern:

```rust
fn main() {
    let id = 5;
    match id {
        small @ 1..=9 => println!("single digit: {small}"),
        big @ 10..=99 => println!("double digit: {big}"),
        _ => println!("big number"),
    }
}
```

```
single digit: 5
```

`small @ 1..=9` says "match a value in 1 through 9, and bind it to `small`." Without `@` you'd have to choose between testing the range (and losing the value) or binding the value (and losing the range test). The `@` gives you both, which is exactly what its quiz-favorite reputation rests on.

> **Key insight**
>
> A pattern does two jobs that an `if` separates: it *tests* a value's shape and *extracts* the parts in one step. `Some(n)`, `90..=100`, `(x, y) if x + y == 0`, `small @ 1..=9`: each both decides whether the arm fires and names the pieces you'll use. That fusion is why `match` replaces long ladders of `if`-`else` and field access with a single readable block.

## Quiz time

**Question #1**

What does this print, and why?

```rust
fn main() {
    let n = 7;
    match n {
        x if x < 0 => println!("negative"),
        0 => println!("zero"),
        1..=9 => println!("small"),
        _ => println!("big"),
    }
}
```

<details class="solution">
<summary>Show solution</summary>

`small`. The first arm's guard `x < 0` is false (7 isn't negative), `0` doesn't match, and `1..=9` matches 7, so that arm fires and the `_` is never reached. Arms are tried top to bottom; the first match wins.

</details>

**Question #2**

Rewrite this `if`-`else` chain as a single `match` with a range pattern and a catch-all:

```rust
fn fee(age: u32) -> u32 {
    if age <= 5 {
        0
    } else if age <= 17 {
        10
    } else {
        20
    }
}
```

<details class="solution">
<summary>Show solution</summary>

```rust
fn fee(age: u32) -> u32 {
    match age {
        0..=5 => 0,
        6..=17 => 10,
        _ => 20,
    }
}
```

The ranges are inclusive (`..=`) and don't overlap; the `_` covers 18 and up. Cleaner than the chain, and the compiler checks it covers every `u32`.

</details>

**Question #3**

Use `@` to write a `match` arm that matches an `i32` in the range 1 through 12, binds it to `month`, and prints `"month N is valid"`. Add a catch-all for everything else.

<details class="solution">
<summary>Show solution</summary>

```rust
fn check(n: i32) {
    match n {
        month @ 1..=12 => println!("month {month} is valid"),
        _ => println!("not a month"),
    }
}
```

`month @ 1..=12` tests the range and binds the value in one pattern, so the arm can both fire only for valid months and print which one.

</details>

These patterns work on more than enums. The next lesson aims the same machinery at structs and tuples: destructuring, which lets a single pattern pull apart a whole compound value at once.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
