Exhaustive Matching
Every match expression in Rust must be exhaustive: its arms must cover every possible value of the scrutinee's type at compile time. If even one enum variant, integer value, or tuple combination is unhandled, the program is rejected with compiler error E0004.
Why Exhaustiveness Matters
In languages with non-exhaustive switch statements, adding a new enum variant silently skips existing switch blocks at runtime. In Rust, adding a new variant to an enum immediately causes the compiler to point out every match expression in the codebase that must be updated to handle the new case.
// ❌ does not compile
enum TaskStatus {
Queued,
Running,
Completed,
Failed(String),
}
fn badge(status: &TaskStatus) -> &'static str {
match status {
TaskStatus::Queued => "WAIT",
TaskStatus::Running => "RUN",
TaskStatus::Completed => "DONE",
}
}
fn main() {}
error[E0004]: non-exhaustive patterns: `&TaskStatus::Failed(_)` not covered
--> src/main.rs:9:11
|
1 | enum TaskStatus {
| --------------- `TaskStatus` defined here
...
5 | Failed(String),
| ------ not covered
...
9 | match status {
| ^^^^^^ pattern `&TaskStatus::Failed(_)` not covered
|
= note: the matched value is of type `&TaskStatus`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
12 ~ TaskStatus::Completed => "DONE",
13 ~ &TaskStatus::Failed(_) => todo!(),
|
Exhaustiveness Across Different Types
| Scrutinee Type | How to Achieve Exhaustiveness |
|---|---|
bool |
Match both true and false |
enum (closed) |
Match all variants explicitly (preferred so future variants trigger compiler errors) or use _ |
#[non_exhaustive] enum (from external crate) |
Must include a wildcard arm _ => ... because external crates may add variants in minor releases |
Integer (u8, i32, usize) |
Cover the full numeric range (0..=255 for u8, i32::MIN..=i32::MAX) or end with a catch-all pattern (_ or other) |
&str / [T] (slice) |
Infinite/dynamic domain; requires a catch-all arm (_ or [..]) |
Empty enum (enum Infallible {}) |
Zero possible values; matched exhaustively by an empty block match val {} |
Exhaustive Integer Range Matching Without a Wildcard
Rust's exhaustiveness checker understands contiguous integer ranges. If your range arms partition the entire range of an integer type (such as 0..=255 for u8), no _ wildcard arm is needed:
fn describe_byte(b: u8) -> &'static str {
match b {
0 => "null byte",
1..=127 => "ASCII byte",
128..=255 => "high byte",
}
}
fn main() {
println!("0 -> {}", describe_byte(0));
println!("65 -> {}", describe_byte(65));
println!("200 -> {}", describe_byte(200));
}
0 -> null byte
65 -> ASCII byte
200 -> high byte
Designing Enums to Avoid Wildcard Masking
When matching on your own domain enums, prefer listing every variant explicitly over ending with _ => .... If you write _ => false, adding a new variant later will silently fall into _ without a compiler warning; if you list all variants explicitly, the compiler guarantees you inspect every call site when the enum evolves.