12.2 match Arms: Exhaustive Pattern Matching
Alright, let’s talk about one of the most brilliant and, frankly, non-negotiable features of match: its insistence on being exhaustive. This isn’t just the language being pedantic; it’s your personal, robotic safety net. It’s the compiler grabbing you by the shoulders, looking you dead in the eye, and saying, “I see you’re handling Some and None, but what if, and hear me out, the value is None?” …Wait, no, that’s not it. It’s smarter than that.
The point is, a match expression in Rust must cover every possible variant of the type you’re matching on. You can’t just handle the cases you think will happen. You must handle the cases that can happen. This eliminates a whole category of runtime errors that plague other languages. It’s the difference between hoping your code works and knowing your code compiles.
Think of an Option<i32>. It has exactly two possibilities: Some(i32) or None. If you write a match that only handles Some, Rust will politely, yet firmly, refuse to compile your code.
let some_number: Option<i32> = Some(42);
// let nothing: Option<i32> = None;
// This will NOT compile. It's non-exhaustive.
let result = match some_number {
Some(n) => format!("The number is {}", n),
};
// Compiler Error: non-exhaustive patterns: `None` not covered
The compiler isn’t guessing. It knows the algebraic data type (Option<T>) inside and out and has a complete list of its variants. Your job is to prove you’ve considered them all.
The Catch-All Arm: _
Sometimes, you genuinely don’t care about every single possibility. Maybe you’re matching on a u8 (which has 256 possible values) and you only want to do something special for a few of them. Writing 256 arms would be absurd. This is where the underscore _ pattern comes in. It’s the ultimate “and everything else” bucket.
let some_value: u8 = 7;
match some_value {
1 => println!("The loneliest number"),
2 | 3 | 5 | 7 => println!("A prime time!"), // Matching multiple values
_ => (), // Do absolutely nothing for 0, 4, 6, 8, 9, 10, ... 255
}
The _ arm must come last because patterns are checked in order. If you put it first, it would greedily match everything, and the other arms would never get a chance. The compiler will also stop you from making that mistake.
(): The Explicit “Do Nothing”
Notice I used () (the unit type) in the _ arm above. This is a common and idiomatic way to say “explicitly do nothing.” It’s far better than leaving the arm empty or using a cryptic comment. It tells the reader, “Yes, I considered all other cases and intentionally want no action taken here.”
The if Guard: Adding Logic to Patterns
What if your exhaustion condition isn’t just about the variant, but also about the data inside it? Enter the if guard. It lets you slap an extra condition on a pattern arm.
let some_number: Option<i32> = Some(10);
match some_number {
Some(n) if n % 2 == 0 => println!("Even Steven: {}", n),
Some(n) => println!("That's odd: {}", n), // Handles all other Some(n)
None => (),
}
Here, the exhaustiveness is still satisfied—we have arms for Some and None. But the first Some arm only fires if its condition is true. If not, the value falls through to the next Some arm. The compiler is smart enough to understand that the second Some(n) catches all the Some cases not caught by the first, so this is still exhaustive.
The Pitfall: Overzealous Catch-Alls
The _ pattern is powerful, but use it with caution. It can silently swallow cases you didn’t anticipate, turning compile-time errors into runtime bugs.
Imagine you define an enum WebEvent with variants PageLoad, PageUnload, and KeyPress(char). You write a match with a catch-all:
# enum WebEvent { PageLoad, PageUnload, KeyPress(char), /* ... */ }
# let event = WebEvent::PageLoad;
match event {
WebEvent::PageLoad => /* ... */,
WebEvent::PageUnload => /* ... */,
_ => (), // I'm "handling" KeyPress and any future variants... by ignoring them.
}
This compiles today. But tomorrow, you or a colleague add a MouseClick { x: i32, y: i32 } variant to WebEvent. The compiler, seeing the _, will happily let this match keep compiling. Your new MouseClick variant will now be silently ignored everywhere this match exists. This is a terrible way to find a bug.
The better practice? Avoid _ when matching on your own enums. Handle every variant explicitly. If you add a new variant, the compiler will then flag every match expression on that enum as non-exhaustive, forcing you to consciously decide what to do at each call site. This isn’t an error; it’s a feature. It’s the compiler guiding you through a refactor.
Only use _ for types with a huge number of variants you truly don’t care about (like primitive integers) or when you’re absolutely certain you want to ignore any future variants (a decision you should make very deliberately). For your own data structures, lean into the exhaustiveness check. It’s one of Rust’s superpowers. Let it do its job.