---
title: 'Pattern Matching'
source: 'https://academia.sh/en/courses/rust-fundamentals/pattern-matching'
course: 'Rust Fundamentals'
language: en
updated: '2026-09-15T04:58:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that match is an expression and that all of its arms must give the same type'
    - 'Verifies that the same value can be taken apart with an exact, a binding, and a guarded pattern, and that all three belong to the same class'
    - 'Explains why the compiler requires exhaustiveness in match arms'
    - 'Shows with an example that a catch-all arm provides exhaustiveness but cannot protect against a new variant'
---

# Pattern Matching

The same Circle(5.0) value is taken apart by three separate pattern forms — exact, binding, guarded — and match itself, as an expression, gives the same type across every arm. The limiting measurement shows that a catch-all arm provides exhaustiveness but can silently swallow a new variant.

The Enums lesson measured how many separate forms a `Shape` value can take and how those
forms are constructed in the source — but it left **reading** which variant the constructed
value carries to two helper functions, in the background. `match` itself was never opened:
which pattern forms exist, when one arm comes before another, why the compiler requires
every variant to be handled? This lesson opens exactly these three questions.

`match` is the construct in Rust that compares a value against patterns and runs the body
of the first matching arm. The Control Flow lesson already introduced `match` as an
expression — it produces a value and every arm must give the same type. This lesson deepens
that definition: with how many different **pattern forms** can the same value be caught,
and do all of these forms really fall into the same class?

Taking values apart with patterns is not itself a new concept in this course; the idea of
branching on the shape of a value has already been established. What this lesson measures
is a requirement specific to Rust's own implementation of that idea: an arm left out
prevents the source from compiling. The scope and limits of this requirement are measured
in detail below.

## match Is an Expression, All Arms Give the Same Type

The result of a `match` expression depends on which arm matches — but the **type** of the
result does not depend on which arm matches. The compiler checks at compile time that every
arm of a `match` gives a value of the same type; if one arm gives `&str` and another gives
`i32`, the source does not compile. This rule is a direct consequence of `match` being an
expression: an expression has a single type, and that type cannot change depending on which
arm runs.

The run below takes apart a single `Shape::Circle(5.0)` value with three separate
functions — three separate pattern forms.

```rust
// three_pattern_forms.rs — match is an expression, the same value is taken apart three ways
enum Shape {
    Empty,
    Circle(f64),
    Rectangle(f64, f64),
}

fn classify_exact(s: &Shape) -> &'static str {
    match s {
        Shape::Empty => "empty",
        Shape::Circle(_) => "circle",
        Shape::Rectangle(_, _) => "rectangle",
    }
}

fn classify_binding(s: &Shape) -> String {
    match s {
        Shape::Empty => "empty".to_string(),
        Shape::Circle(radius) => format!("circle radius={}", radius),
        Shape::Rectangle(width, height) => format!("rectangle {}x{}", width, height),
    }
}

fn classify_guarded(s: &Shape) -> &'static str {
    match s {
        Shape::Circle(radius) if *radius > 3.0 => "large circle",
        Shape::Circle(_) => "small circle",
        Shape::Rectangle(width, height) if width == height => "square",
        Shape::Rectangle(_, _) => "rectangle",
        Shape::Empty => "empty",
    }
}

fn main() {
    let value = Shape::Circle(5.0);
    println!("{:<20}: {}", "exact pattern", classify_exact(&value));
    println!("{:<20}: {}", "binding pattern", classify_binding(&value));
    println!("{:<20}: {}", "guarded pattern", classify_guarded(&value));

    let small = Shape::Circle(1.0);
    println!("{:<20}: {}", "guarded (small)", classify_guarded(&small));

    let square = Shape::Rectangle(4.0, 4.0);
    println!("{:<20}: {}", "guarded (square)", classify_guarded(&square));

    let empty = Shape::Empty;
    println!("{:<20}: {}", "exact (empty)", classify_exact(&empty));
}
```

```
exact pattern       : circle
binding pattern     : circle radius=5
guarded pattern     : large circle
guarded (small)     : small circle
guarded (square)    : square
exact (empty)       : empty
```

**DM8.** — The claim "every arm of `match` gives the same type" is measured here by the
three functions' own return-type signatures: `classify_exact` and `classify_guarded`
declare in their signature that they return `&'static str`, `classify_binding` declares
that it returns `String`. Each `match` expression inside the three functions matches the
single type in its own signature; otherwise the source would not compile at all.

## The Same Value Taken Apart by Three Pattern Forms

`classify_exact`, `classify_binding`, and `classify_guarded` all operated on the same
`value` — `Shape::Circle(5.0)` — but each caught the value with a different pattern form.

**The exact pattern** looks only at which variant matched; it never touches the carried
data. In the arm `Shape::Circle(_) => "circle"`, the underscore (`_`) means "whatever is in
this position" — whether `radius`'s value is `5.0` or `0.3` makes no difference, the arm
only checks whether the variant is `Circle`. This is exactly what the `variant_name`
function did in the previous lesson; there too, every variant was distinguished only by its
name, without ever reading the data it carried.

**The binding pattern** does not stop at matching the variant, it also binds the carried
data to a name. In the arm `Shape::Circle(radius) => ...`, the name `radius` is bound to the
`5.0` inside the matched value, and it can be used inside the arm's body, placed into a text
template. The `circle radius=5` seen in the output is exactly the result of this binding.

The difference between the binding pattern and the exact pattern is not just "whether a
name is written or not"; the bound name is a variable genuinely **usable** in the arm's
body. In the `classify_binding` function, `radius` is passed directly as an argument
filling a text template — it behaves exactly like an ordinary function parameter. The
underscore (`_`) in the exact pattern, on the other hand, is not a name; it binds to
nothing — it only says "there is a value here, but I am not interested in it."

**The guarded pattern** adds a condition (`if`) on top of a binding pattern; in Rust this is
called a match guard. The arm `Shape::Circle(radius) if *radius > 3.0 => "large circle"`
matches only when both the variant is `Circle` and the bound `radius` is greater than
`3.0`. Since `value`'s radius is `5.0`, this condition is satisfied and the result is
`"large circle"`; the `small` value (`Circle(1.0)`) does not fall into the same arm,
because the condition is not satisfied, and it falls to the arm below,
`Shape::Circle(_) => "small circle"`.

**DM9.** — The claim that the three pattern forms are all "in the same class" is measured
by all three producing a value as the body of a `match` expression: none of them was
written for a side effect, all three were directly the return value of a function. Whether
a pattern is exact, binding, or guarded does not change whether `match` itself is an
expression — all three are instances of the same `match ... { pattern => value, ... }`
form.

The square example takes this a step further: the arm `Shape::Rectangle(width, height) if
width == height => "square"` binds two fields at once and compares them to each other.
Since the `square` value is `Rectangle(4.0, 4.0)`, this condition is also satisfied. A match
guard can use not just a single bound name but several bound names at once — the binding
itself is the pattern's job, the condition itself is an ordinary logical expression. The
comparison `width == height` is not a syntax specific to `match`; it follows the same rules
as comparing any two decimal numbers in any context, it is just applied here to the two
names the pattern bound.

The arm order in the `classify_guarded` function is not arbitrary. The arm `Shape::Circle
(radius) if *radius > 3.0` is written **before** the unconditioned arm
`Shape::Circle(_) => "small circle"`; `match` arms are tried in order from top to bottom,
and the first matching arm wins. If these two arms swapped places, the pattern `Circle(_)`,
carrying no condition at all, would catch every `Circle` value — large or small — and no
value would ever reach the conditioned arm below. The same rule applies to the `Rectangle`
arms: the square check has to come before the unconditioned `Rectangle` arm, otherwise
every rectangle — square or not — would fall into the general arm.

This ordering requirement does not show up in the `classify_exact` and `classify_binding`
functions, because none of the arms there is a subset of another: `Empty`, `Circle(_)`, and
`Rectangle(_, _)` are disjoint variants, and they catch the same value no matter what order
they are written in. Ordering only becomes significant when more than one arm targets the
same variant — as with the guarded patterns.

## Exhaustiveness: Why a Missing Arm Fails to Compile

`match` carries a constraint specific to Rust: every arm together must cover **every**
possible variant of the matched value. This is called exhaustiveness. The
`classify_exact` function above wrote three arms — `Shape::Empty`, `Shape::Circle(_)`, and
`Shape::Rectangle(_, _)` — all three variants in `Shape`'s declaration. If one of these
arms, say the line `Shape::Rectangle(_, _) => "rectangle"`, were removed from the source,
the remaining source would not compile.

This lesson does not place source that fails to compile into a code block, because source
that does not compile never enters a block in the first place — a rejected piece of writing
has no output, and therefore produces no number to verify by running it; but the reason for
the rejection can still be described here. When the compiler sees an incomplete piece of
writing like `match s { Shape::Empty => ..., Shape::Circle(_) => ... }`, it knows that `s`
can carry a `Shape::Rectangle` value at runtime — because that is a variant the `enum
Shape` declaration allows. Since none of the arms present covers that possibility, the
compiler cannot say which arm would run, and it rejects the source. This rejection ensures
that an "unexpected value" error never occurs at runtime at all: a missing arm is caught
before the program runs, during compilation.

**DM10.** — The claim that exhaustiveness is genuinely required is not measured here by a
separate compiler invocation — since source that fails to compile does not enter a code
block — but there is indirect proof: each of the three functions above covers **all** three
of `Shape`'s variants. If exhaustiveness were not required, there would be no reason to
write out all three arms in full; all three are there only because they are necessary.

The exhaustiveness check operates by looking at the type of the value the `match` in the
source matches; it does not look at what values the carried data can take. `Shape::Circle
(f64)` can carry an uncountably large number of radius values, but the compiler does not
check this infinity one by one — it only asks whether the `Circle` variant is handled in
some arm. The distinction measured in the Enums lesson — "variant count is not state
count" — holds here too: exhaustiveness is a guarantee at the variant level, not the state
level. As long as one arm writes `Shape::Circle(radius)`, that variant is considered
covered, regardless of which value `radius` carries.

## Limiting Measurement: a Catch-All Arm Provides Exhaustiveness but Does Not Protect

There is a shortcut for satisfying the exhaustiveness rule: writing `_ => ...` as the last
arm. This arm covers all remaining variants — however many there are in the source — in a
single stroke and satisfies the compiler. But this satisfaction does not mean the arm
genuinely considers every variant **separately**; it only means none of them was left out.

```rust
// exhaustiveness_catchall.rs — a catch-all arm provides exhaustiveness but does not protect
enum OldShape {
    Circle(f64),
    Rectangle(f64, f64),
}

enum NewShape {
    Circle(f64),
    Rectangle(f64, f64),
    #[allow(dead_code)]
    Triangle(f64, f64, f64),
}

fn label_old(s: &OldShape) -> String {
    match s {
        OldShape::Circle(radius) => format!("circle r={}", radius),
        OldShape::Rectangle(width, height) => format!("rectangle {}x{}", width, height),
    }
}

fn label_new(s: &NewShape) -> String {
    match s {
        NewShape::Circle(radius) => format!("circle r={}", radius),
        NewShape::Rectangle(width, height) => format!("rectangle {}x{}", width, height),
        _ => "other".to_string(),
    }
}

fn main() {
    let old_circle = OldShape::Circle(2.0);
    let old_rectangle = OldShape::Rectangle(2.0, 2.0);
    println!("{:<20}: {}", "old: circle", label_old(&old_circle));
    println!("{:<20}: {}", "old: rectangle", label_old(&old_rectangle));

    let new_circle = NewShape::Circle(2.0);
    let new_rectangle = NewShape::Rectangle(2.0, 2.0);
    let new_triangle = NewShape::Triangle(3.0, 4.0, 5.0);
    println!("{:<20}: {}", "new: circle", label_new(&new_circle));
    println!("{:<20}: {}", "new: rectangle", label_new(&new_rectangle));
    println!("{:<20}: {}", "new: triangle", label_new(&new_triangle));
}
```

```
old: circle         : circle r=2
old: rectangle      : rectangle 2x2
new: circle         : circle r=2
new: rectangle      : rectangle 2x2
new: triangle       : other
```

`OldShape` carries two variants, and `label_old` handles both explicitly, by name — there is
no catch-all arm, because the two arms already provide exhaustiveness on their own. Then
the source moves to `NewShape`: the same two variants (`Circle`, `Rectangle`) are kept, but
a third variant (`Triangle`) is added. The `label_new` function carries the same two arms as
`label_old`, and instead of writing a third arm, adds `_ => "other".to_string()`.

This source compiles without a problem — exhaustiveness is satisfied, because `_` covers
every variant. But when a value with real data like `Triangle(3.0, 4.0, 5.0)` enters this
code, the result is only `"other"`: the triangle's three side lengths (`3.0`, `4.0`, `5.0`)
are never read anywhere, never factored into any computation. Even though `Triangle` is a
different variant from `Circle` and `Rectangle`, it receives no separate treatment at the
code level.

**DM11.** — The claim "a catch-all arm provides exhaustiveness but does not protect" is
measured here by running two enums side by side: `OldShape` and `NewShape` carry the same
two variants (`Circle`, `Rectangle`) and give the same result for the same two inputs
(`2.0` and `2.0, 2.0`) — `"circle r=2"` and `"rectangle 2x2"`. The only difference is that
`NewShape` adds a third variant and `label_new` leaves that variant to `_`. The output's
last line — `new: triangle : other` — directly shows that this new variant falls to the
catch-all without receiving treatment of its own.

This sets firm boundaries on what exhaustiveness guarantees. The compiler only answers yes
to the question "is there **one** arm for every variant"; it does not answer the question
"was every variant handled with its own logic." Adding a variant to an enum later lets a
`match` with a `_` arm pass through without a compile error — no notice comes from the
compiler for the developer to notice the new variant, because the exhaustiveness rule is
already satisfied. The situation is different for a `match` without a catch-all: the moment
a new variant is added, every `match` that does not cover that variant stops compiling and
takes the developer directly to that line.

This difference is also visible in how `label_old` and `label_new` are written.
`label_old`, because it writes both of `OldShape`'s variants by name, would stop compiling
if a third variant were added to `OldShape` — the code would directly show which line was
missing. `label_new`, because it carries a `_` arm, keeps compiling even if a fourth variant
were added to `NewShape`; the new variant, even though it resembles none of the previous
three, falls into the same `"other"` result. Both functions satisfy the exhaustiveness
rule, but only one of them puts a future change squarely in front of the developer's eyes.

These three lessons — structs, enums, pattern matching — each answered a separate question
about a data model: which fields a value carries, how many separate forms it can take, and
how those forms are read from the source. So far every measurement was made on a single
value — a single `Point`, a single `Shape`. But a real program's data is almost never made
up of a single value; a large number of values have to be held together at once.

## Summary

- `match` is an expression; all of its arms must give a value of the same type, otherwise
  the source does not compile.
- The same value can be taken apart with an exact pattern (checking only the variant), a
  binding pattern (binding the data to a name), and a guarded pattern (adding a condition to
  the bound data); all three are instances of the same `match` structure.
- Exhaustiveness requires a `match` to cover every possible variant of the matched value; a
  variant left out makes the source fail to compile.
- A catch-all arm (`_`) provides exhaustiveness but does not protect: it silently covers a
  variant added later without handling it with its own logic.

## Next Step

This lesson measured how a single `Shape` value is taken apart. But real programs hold not
a single value but a large number of values at once — every shape in a list, every name in
a dictionary. So far every lesson has treated a single value bound to a single variable; the
next lesson lifts that limit and carries the same question to a group of values: how are a
large number of values held at the same time, and which class do the mutating methods of
these collections (adding, removing) fall into?
