Enums & Pattern Matching
Tagged unions, exhaustive match, if-let / let-else, refutable patterns.
Enums & Pattern Matching
If ownership is what makes Rust safe, exhaustive pattern matching is what makes it pleasant to refactor. Add a new variant to an enum and the compiler points at every place you forgot to update. No grep, no audit, no "I'll catch it in code review". The type system catches it.
Recap: enums are tagged unions
enum Message {
Quit, // unit variant
Move { x: i32, y: i32 }, // struct-like variant
Write(String), // tuple variant (single field)
ChangeColor(u8, u8, u8), // tuple variant (multiple fields)
}
Each variant carries its own data. A Message is exactly one of these at any moment.
match — the exhaustive switch
fn handle(msg: Message) -> String {
match msg {
Message::Quit => String::from("bye"),
Message::Move { x, y } => format!("move to ({}, {})", x, y),
Message::Write(s) => s,
Message::ChangeColor(r, g, b) => format!("rgb({},{},{})", r, g, b),
}
}
If you delete one of those arms, the compiler emits:
error[E0004]: non-exhaustive patterns: `Quit` not covered
Add a variant to Message, and every match on Message in the codebase gets the same error. This is the killer feature — refactor with the compiler instead of without it.
Match arms bind data
The pattern on the left of => destructures the variant and binds names you can use on the right:
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle { base, height } => 0.5 * base * height,
}
Field shorthand works in patterns too: Circle { radius } instead of Circle { radius: radius }.
Wildcards: _ and ..
_ matches anything but binds nothing:
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(_) => 25, // ignore the state inside Quarter
}
The bare _ arm at the bottom of a match catches the rest:
match key {
"save" => save(),
"quit" => quit(),
_ => println!("unknown command"),
}
.. skips the rest of a struct or tuple:
match user {
User { id, .. } => println!("id = {}", id), // ignore name, email, etc.
}
Matching literals and ranges
match n {
0 => "zero",
1..=9 => "single digit",
10..=99 => "two digits",
_ => "many",
}
match c {
'a'..='z' => "lowercase ascii letter",
'A'..='Z' => "uppercase ascii letter",
_ => "something else",
}
1..=9 is an inclusive range pattern — the only pattern form that uses ..=.
Or-patterns: |
match c {
'a' | 'e' | 'i' | 'o' | 'u' => "vowel",
_ => "not a vowel",
}
Multiple patterns on a single arm. Bindings must agree across alternatives:
match msg {
Message::Move { x, .. } | Message::ChangeColor(x, _, _) => println!("{}", x),
_ => {}
}
Match guards — if
A guard refines a pattern with a boolean:
match age {
n if n < 0 => panic!("negative age"),
0..=12 => "child",
13..=19 => "teen",
_ => "adult",
}
Important: guards don't count for exhaustiveness. match Some(x) { Some(x) if x > 0 => ..., None => ... } is not exhaustive — you still need a fall-through for Some(x) where the guard fails.
if let — match one variant
When you only care about one of several variants:
if let Some(x) = maybe_user {
println!("user: {}", x);
}
// equivalent to:
match maybe_user {
Some(x) => println!("user: {}", x),
None => {}
}
if let ... else is also valid:
if let Some(cfg) = load_config() {
run(cfg);
} else {
eprintln!("missing config");
}
let else — Rust 1.65+
When a pattern should match but you want to early-exit if it doesn't, instead of nesting if let:
fn run(cfg: Option<Config>) {
let Some(cfg) = cfg else {
eprintln!("missing config");
return; // diverging — must return, break, continue, or panic
};
// cfg is bound in the surrounding scope from here onwards
println!("running with port {}", cfg.port);
}
The else block must diverge — return, break, continue, or panic. The binding is scoped to the rest of the function. This eliminates the right-drift you'd otherwise get from chained if lets.
while let — loop until pattern fails
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
// loop ends when pop() returns None
Refutable vs irrefutable patterns
A pattern is irrefutable if it always matches:
let (a, b) = (1, 2); // tuple pattern matches any 2-tuple
let User { id, name } = u; // struct pattern matches the struct
A pattern is refutable if it might not match:
let Some(x) = opt; // ❌ refutable not allowed in plain `let`
// (opt could be None)
let binding requires irrefutable. if let, while let, let-else, and match arms accept refutable. The reason: a plain let has no fall-through path; a refutable pattern there would be a runtime crash waiting to happen.
@ bindings — match a pattern AND keep the value
match age {
n @ 13..=19 => println!("teen of age {}", n), // n binds here
n @ 0..=12 => println!("child of age {}", n),
_ => println!("other"),
}
name @ pattern lets you bind the matched value to a name and assert its shape.
Putting it together — a real example
enum Event {
KeyPress { key: char, ctrl: bool },
Click { x: i32, y: i32 },
Resize(u32, u32),
}
fn dispatch(e: Event) {
match e {
Event::KeyPress { key: 'q', ctrl: true } => quit(),
Event::KeyPress { key, ctrl: false } if key.is_ascii() => insert_char(key),
Event::KeyPress { .. } => {} // ignore other key combos
Event::Click { x, y } => handle_click(x, y),
Event::Resize(w, h) if w > 0 && h > 0 => layout(w, h),
Event::Resize(_, _) => {}
}
}
Five arms, mixed literal/struct/tuple/wildcard patterns, two guards. The compiler verifies coverage. Add Event::Scroll { dy: f32 } tomorrow and dispatch won't compile until you handle it.
When to reach for what
| You want | Use |
|---|---|
| Branch on every variant | match |
| Branch on one variant, ignore the rest | if let |
| Bind a value and bail if the pattern doesn't match | let-else |
| Loop while the pattern matches | while let |
| Check a refined condition on a matched value | match guard if expr |
| Match value AND bind it | name @ pattern |
| Catch the leftovers | _ => |
Pattern matching is one of the highest-leverage features in Rust. It pairs with sum types to make illegal states unrepresentable — and the compiler keeps you honest as the system evolves.
Tools in the wild
4 tools- clirustcfree tier
Exhaustiveness errors are checked here — adding an enum variant lights up every incomplete match site.
- cliclippyfree tier
Lints `match_single_binding`, `redundant_pattern_matching`, `match_wildcard_for_single_variants`.
- servicerust-analyzerfree tier
Editor LSP — `Fill match arms` quick-action generates every missing variant scaffold.
- clicargo-expandfree tier
See how `if let`, `while let`, and `?` desugar to plain `match` expressions.