Rust Pattern Matching
debt(d6/e3/b3/t7)
Closest to 'specialist tool catches it' (d5), but bumped to d6: clippy can flag the `_ => ` code_pattern on owned enums, yet the silent-swallow danger only surfaces when a future variant is added and the catch-all quietly absorbs it, so detection sits between specialist tool and careful review.
Closest to 'simple parameterised fix' (e3): the quick_fix is to replace a `_ =>` catch-all with explicit arms for each variant, a localised pattern swap within one match expression rather than a single-line change or cross-file refactor.
Closest to 'localised tax' (b3): although match applies to all Rust contexts (web/cli/queue/library), the consequence of a catch-all is confined to the component owning that enum and match; it does not impose system-wide gravitational pull.
Closest to 'serious trap' (t7): the misconception that match is just a switch with a harmless default contradicts Rust's exhaustiveness guarantee, plus bare lowercase names bind new variables instead of comparing constants and arm ordering shadows specific patterns — these contradict how switch/case behaves in other languages.
Also Known As
TL;DR
Explanation
Pattern matching is one of Rust's defining features. The `match` expression compares a value against a series of patterns and runs the arm whose pattern matches first. Unlike a C-style switch, `match` is an expression that yields a value, and it is exhaustive: the compiler refuses to compile a `match` that fails to cover every possible case. This exhaustiveness check is the heart of why pattern matching matters, because it turns 'I forgot to handle that variant' from a runtime surprise into a compile error.
Patterns can destructure almost any value: enum variants like `Some(x)` or `Ordering::Less`, tuples `(a, b)`, structs `Point { x, y }`, slices `[first, rest @ ..]`, ranges `1..=5`, and literals. Bindings introduced in a pattern, such as the `x` in `Some(x)`, are available in that arm's body. Guards add a boolean condition with `if`, and the binding operator `@` lets you capture a value while also testing it against a sub-pattern. The wildcard `_` matches anything without binding, and a bare name like `other` matches anything and binds it, which is the idiomatic catch-all.
For the common case of caring about only one variant, `if let` and `while let` are concise sugar over `match`. `let else` (stable since 1.65) binds a pattern or diverges - returning, breaking, or panicking - in the `else` block, which keeps the happy path unindented. The newer `matches!` macro returns a boolean for quick membership tests.
The most important discipline is avoiding a needless `_ => {}` catch-all on your own enums. A wildcard silences the exhaustiveness check, so when you later add a variant the compiler stays silent instead of pointing you at every place that must be updated. Spelling out each variant, or grouping the genuinely identical ones, preserves that guarantee. Pattern matching combined with enums and `Option`/`Result` is how Rust models state precisely and lets the compiler prove you handled it all.
Common Misconception
Why It Matters
Common Mistakes
- Adding a `_ => {}` catch-all to your own enum so the compiler stops warning when you add a new variant later.
- Using a deep nested match to test a single variant when `if let` or `matches!` would be far clearer.
- Forgetting that a bare lowercase name in a pattern binds a new variable rather than comparing against a constant.
- Writing arms in the wrong order so a broad pattern shadows a more specific one that can never match.
- Cloning or moving out of a matched value unnecessarily instead of matching on a reference with `ref` or `&`.
Avoid When
- Matching on a foreign or non-exhaustive enum marked #[non_exhaustive], where a wildcard arm is required by the compiler.
- Matching on an open-ended type like an integer or string where enumerating every value is impossible and a catch-all is the only option.
- Quick boolean checks where the `matches!` macro reads more clearly than a full match expression.
When To Use
- Handling every variant of an enum you own so the compiler forces updates when the enum grows.
- Destructuring Option, Result, tuples, or structs to bind inner values cleanly in one place.
- Branching on the shape of data with guards and ranges instead of chained if/else comparisons.
- Extracting a single variant concisely with if let or let else to keep the happy path unindented.
Code Examples
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Scroll(i32),
}
fn handle(event: &Event) -> String {
match event {
Event::Click { x, y } => format!("click at {},{}", x, y),
// The catch-all silences the exhaustiveness check.
// When a new variant is added, this arm swallows it silently.
_ => "unhandled".to_string(),
}
}
fn main() {
let e = Event::KeyPress('a');
// KeyPress and Scroll both fall into the catch-all unnoticed.
println!("{}", handle(&e));
}
enum Event {
Click { x: i32, y: i32 },
KeyPress(char),
Scroll(i32),
}
fn handle(event: &Event) -> String {
// Every variant is spelled out, so adding one is a compile error
// until this match is updated.
match event {
Event::Click { x, y } => format!("click at {},{}", x, y),
Event::KeyPress(c) => format!("key {}", c),
Event::Scroll(delta) => format!("scroll {}", delta),
}
}
fn main() {
let e = Event::KeyPress('a');
println!("{}", handle(&e));
// if let for the single-variant case keeps the happy path flat.
if let Event::Scroll(delta) = &e {
println!("scrolled {}", delta);
}
}