Rust 25 🦀 Exhaustive Pattern Matching Rules
The compiler accepts a match expression only when the patterns cover every possible value of the matched type. This requirement is called exhaustiveness, and it is enforced at compile time. It is the feature that turns pattern matching from a convenient dispatch mechanism into a safety guarantee. If you add a variant to an enum, every match that does not handle it becomes a compile error. The compiler tells you exactly which code needs updating, and the program cannot be built until every case is considered.
This chapter covers the rules that govern exhaustiveness in Rust. You will learn what it means for a match to be exhaustive, how the compiler decides whether patterns cover a type, which patterns make a match non-exhaustive, and how the _ wildcard interacts with the check. You will see how exhaustiveness applies to enums, references, tuples, and nested patterns, and how the non_exhaustive attribute changes the rules across crate boundaries. You will also see the practical consequences: exhaustive matching forces consideration of every state, and that force is the point.
Key point: A match is exhaustive when its patterns cover every possible value of the matched type. The compiler proves this by analyzing the patterns. If it cannot prove coverage, the code does not compile. The wildcard _ always makes a match exhaustive, but using it defeats the compiler’s ability to detect missing cases when the type later changes.
Why exhaustiveness exists
The safety problem. A non-exhaustive match would have to do something when no pattern matches. Languages that allow partial matches—C’s switch, Java’s switch, OCaml’s match with a warning—either fall through silently, throw an exception at runtime, or produce a warning that developers ignore. Rust makes the missing case a compile error. There is no runtime branch for “no pattern matched” because such a situation cannot occur.
The refactoring problem. When a new variant is added to an enum, every match on that enum that does not handle the new variant becomes invalid. The compiler reports each one. This means the program cannot be compiled until every location that consumes the enum has been updated. The alternative—silent fallthrough—means new variants are handled incorrectly or ignored until a bug appears at runtime. Exhaustiveness turns a class of runtime bugs into a compile-time checklist.
The wildcard problem. The wildcard pattern _ matches anything, which makes a match exhaustive by definition. This is sometimes necessary when matching on a type with infinitely many values (like i32) or when the remaining cases genuinely share behavior. But using _ on an enum defeats the exhaustiveness check. Adding a new variant will not produce an error if a _ arm is present. The compiler cannot know whether the new variant should be handled by the wildcard or by a new explicit arm.
The type problem. Exhaustiveness is a property of the matched type. bool has two values; a match with arms for true and false is exhaustive. u8 has 256 values; a match with arms for 0, 1, and _ is exhaustive because the wildcard covers the rest. An enum with three variants needs arms for all three, or a wildcard. A reference to an enum is matched by patterns that destructure through the reference, and exhaustiveness applies to the underlying type.
The cross-crate problem. Types defined in other crates may change. A non-exhaustive enum in a library can add variants without breaking downstream code, because the library marks the enum with #[non_exhaustive]. But this changes the rules: a match on such an enum must include a wildcard arm, because the compiler cannot know how many variants exist in future versions. The non_exhaustive attribute is a deliberate trade-off between exhaustiveness and evolvability.
a. What exhaustiveness means
A match is exhaustive if every possible value of the matched type is matched by at least one pattern. The compiler determines this by analyzing the patterns and the type.
For an enum, the possible values are the variants. A match must have an arm for each variant, or a wildcard that covers the rest.
enum Direction {
North,
South,
East,
West,
}
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "north",
Direction::South => "south",
Direction::East => "east",
Direction::West => "west",
}
}
This match is exhaustive because all four variants are handled. Removing any arm makes the match non-exhaustive, and the compiler reports the missing variant.
For a type with a finite set of values, exhaustiveness can be proven by enumeration. For bool, two arms suffice. For an enum, one arm per variant. For a type with infinitely many values—i32, u32, String—exhaustiveness requires a wildcard or a pattern that matches all values.
fn describe(n: i32) -> &'static str {
match n {
0 => "zero",
1 => "one",
_ => "many",
}
}
The wildcard covers every value not matched by the literal patterns. Without it, the match would be non-exhaustive because i32 has values beyond 0 and 1.
For references, the patterns match through the reference. Exhaustiveness is determined by the underlying type:
fn describe(d: &Direction) -> &'static str {
match d {
Direction::North => "north",
Direction::South => "south",
Direction::East => "east",
Direction::West => "west",
}
}
This match is exhaustive because Direction has four variants. Match ergonomics makes the bindings references automatically.
For tuples, exhaustiveness requires covering every combination of values:
fn describe(pair: (bool, bool)) -> &'static str {
match pair {
(true, true) => "both true",
(true, false) => "first true",
(false, true) => "second true",
(false, false) => "both false",
}
}
All four combinations of bool values are covered. Missing one makes the match non-exhaustive.
b. Patterns that affect exhaustiveness
Several pattern forms interact with exhaustiveness in specific ways.
Literal patterns cover the values they match. A match on bool with arms for true and false is exhaustive because those are the only values of bool.
match flag {
true => "yes",
false => "no",
}
Range patterns cover a range of values. A match on a u8 with arms for 0..=127 and 128..=255 is exhaustive because the ranges cover the entire type.
match byte {
0..=127 => "ascii",
128..=255 => "extended",
}
The compiler checks that ranges do not overlap and that they cover the type. Overlapping ranges produce an unreachable pattern warning.
Or patterns combine alternatives. 1 | 2 | 3 matches any of those values. A match with arms for 1 | 2 | 3, 4..=9, and _ is exhaustive because the wildcard covers the rest.
match n {
1 | 2 | 3 => "small",
4..=9 => "medium",
_ => "large",
}
Enum variant patterns cover the variants they name. For an enum with data-carrying variants, the pattern must match the variant and can bind or ignore the data.
match msg {
Message::Quit => "quit",
Message::Move { .. } => "move",
Message::Write(_) => "write",
}
The .. and _ in the patterns ignore the data but still match the variant. The match is exhaustive if all variants are covered.
Binding patterns like n match anything and make the match exhaustive on their own. A single binding arm n => ... covers every value.
match n {
x => println!("{}", x),
}
This is exhaustive but useless because it does not discriminate. Binding patterns are useful in combination with guards, which do not affect exhaustiveness—a guarded arm does not count toward coverage because the guard might be false.
match n {
x if x > 0 => "positive",
_ => "not positive",
}
The first arm has a guard, so it does not guarantee coverage of positive values. The wildcard is required for exhaustiveness. Without it, the match would be non-exhaustive.
Wildcard patterns _ match anything and guarantee exhaustiveness. They are the escape hatch when the other patterns do not cover the type.
Rest patterns .. ignore remaining fields in structs and tuples. They do not affect exhaustiveness directly; they only specify which fields to ignore.
match point {
Point { x: 0, .. } => "on y-axis",
Point { .. } => "elsewhere",
}
The first arm matches points with x == 0, ignoring y. The second matches everything else. The match is exhaustive because the second arm’s .. covers all fields and the pattern matches any point.
c. Non-exhaustive enums and the wildcard
Some enums are marked with #[non_exhaustive]. This attribute tells the compiler that the enum may gain new variants in future versions, and it affects matching across crate boundaries.
#[non_exhaustive]
pub enum Error {
NotFound,
PermissionDenied,
// Future variants may be added
}
Within the crate that defines the enum, matching is still exhaustive without a wildcard, because the compiler knows the current variants. But in another crate that uses this enum, a match must include a wildcard arm:
match error {
Error::NotFound => "not found",
Error::PermissionDenied => "permission denied",
_ => "other error",
}
The wildcard is required because the downstream crate cannot know what variants might be added. This is a deliberate trade-off: the library can add variants without breaking downstream code, but downstream code must handle the unknown case with a wildcard.
The same attribute can be applied to structs, making it impossible to construct them with a struct literal outside the defining crate. This is used for types that should only be constructed by the library.
The wildcard has a downside. On an exhaustive enum, using _ instead of naming the remaining variants means the compiler will not report a missing case when a new variant is added. The _ arm silently absorbs it, possibly with the wrong behavior. The lint clippy::wildcard_enum_match_arm warns about this. In code that wants to detect new variants, prefer naming all variants explicitly and avoiding _ on enums.
// Preferred: compiler catches new variants
match direction {
Direction::North => { }
Direction::South => { }
Direction::East => { }
Direction::West => { }
}
// Discouraged: new variants silently fall into _
match direction {
Direction::North => { }
_ => { }
}
The first form is more verbose but is the reason exhaustiveness checking is useful. When a new variant is added, the compiler reports each incomplete match. The second form hides the change.
Complete Example Session
// ============================================
// PART 1: EXHAUSTIVE ENUM MATCH
// ============================================
enum Direction { North, South, East, West }
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "north",
Direction::South => "south",
Direction::East => "east",
Direction::West => "west",
}
}
// ============================================
// PART 2: NON-EXHAUSTIVE (COMPILE ERROR)
// ============================================
enum Color { Red, Green, Blue }
fn describe_color(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
// Missing Blue: compile error
}
}
// ============================================
// PART 3: EXHAUSTIVE WITH WILDCARD
// ============================================
fn describe_color(c: Color) -> &'static str {
match c {
Color::Red => "red",
Color::Green => "green",
_ => "other",
}
}
// ============================================
// PART 4: EXHAUSTIVE BOOL
// ============================================
fn describe_bool(b: bool) -> &'static str {
match b {
true => "yes",
false => "no",
}
}
// ============================================
// PART 5: RANGE COVERAGE
// ============================================
fn describe_byte(b: u8) -> &'static str {
match b {
0..=127 => "ascii",
128..=255 => "extended",
}
}
// ============================================
// PART 6: OR PATTERNS
// ============================================
fn describe_digit(n: u8) -> &'static str {
match n {
0 => "zero",
1 | 2 | 3 => "small",
4..=9 => "medium",
_ => "large",
}
}
// ============================================
// PART 7: GUARDS DO NOT COUNT
// ============================================
fn classify(n: i32) -> &'static str {
match n {
x if x > 0 => "positive",
x if x < 0 => "negative",
0 => "zero",
// No wildcard needed: 0 covers the remaining case
}
}
// ============================================
// PART 8: GUARDS REQUIRING WILDCARD
// ============================================
fn classify_partial(n: i32) -> &'static str {
match n {
x if x > 0 => "positive",
_ => "non-positive",
// Wildcard needed: guard does not cover all positive cases
}
}
// ============================================
// PART 9: TUPLE EXHAUSTIVENESS
// ============================================
fn describe_pair(pair: (bool, bool)) -> &'static str {
match pair {
(true, true) => "both true",
(true, false) => "first true",
(false, true) => "second true",
(false, false) => "both false",
}
}
// ============================================
// PART 10: NON_EXHAUSTIVE ENUM
// ============================================
#[non_exhaustive]
pub enum ApiError {
NotFound,
Unauthorized,
}
fn handle(err: ApiError) -> &'static str {
match err {
ApiError::NotFound => "not found",
ApiError::Unauthorized => "unauthorized",
// Wildcard required outside the defining crate
_ => "unknown",
}
}
The ten parts covered exhaustive enum matching, non-exhaustive compile errors, wildcard coverage, boolean exhaustiveness, range coverage, or patterns, guards and coverage, guards requiring wildcards, tuple exhaustiveness, and the non_exhaustive attribute.
Quick Reference
Exhaustiveness by Type
| Type | Exhaustive Match Requires |
|---|---|
bool | Arms for true and false |
| Enum (exhaustive) | Arm for every variant |
Enum (non_exhaustive) | Arm for every variant + wildcard |
u8 | Ranges covering 0..=255 or wildcard |
i32 | Wildcard or binding |
| Tuple | All combinations of component values |
| Reference | Same as underlying type |
Pattern Coverage
| Pattern | Covers |
|---|---|
| Literal | The exact value |
| Range | All values in range |
| Or | Union of alternatives |
| Enum variant | That variant |
| Binding | Everything |
Wildcard _ | Everything |
| Guard | Nothing (guards do not contribute) |
Common Compiler Errors
| Error | Cause | Fix |
|---|---|---|
non-exhaustive patterns | Missing variant or value | Add arm or wildcard |
unreachable pattern | Earlier arm catches all | Remove or reorder |
unreachable pattern with guard | Guard always true | Remove guard or reorder |
| Missing wildcard | non_exhaustive enum | Add _ arm |
The Wildcard Trade-off
| Use Wildcard | Avoid Wildcard |
|---|---|
Infinite types (i32) | Exhaustive enums |
non_exhaustive enums | Internal enums |
| Genuinely shared behavior | Cases that should be explicit |
| Prototyping | Production code |
Best Practices
✅ Do This:
// Name all enum variants explicitly
match direction {
Direction::North => { }
Direction::South => { }
Direction::East => { }
Direction::West => { }
} // ✅
// Use wildcard for infinite types
match n {
0 => "zero",
_ => "other",
} // ✅
// Use ranges that cover the type
match byte {
0..=127 => "ascii",
128..=255 => "extended",
} // ✅
// Use wildcard for non_exhaustive enums
match err {
ApiError::NotFound => { }
_ => { }
} // ✅
// Use guards with a fallback
match n {
x if x > 0 => "positive",
_ => "non-positive",
} // ✅
❌ Don’t Do This:
// Don't use wildcard on exhaustive enums
match direction {
Direction::North => { }
_ => { } // hides new variants // ❌
}
// Don't rely on guards for exhaustiveness
match n {
x if x > 0 => "positive",
x if x <= 0 => "non-positive",
// Exhaustive in practice, not to the compiler // ❌
}
// Don't forget ranges leave gaps
match byte {
0..=100 => "low",
200..=255 => "high",
// 101..=199 missing: compile error // ❌
}
// Don't put wildcard before specific arms
match n {
_ => "other",
0 => "zero", // unreachable // ❌
}
// Don't assume guards cover values
match n {
x if x > 0 => "positive",
// No arm for x <= 0: compile error // ❌
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Non-exhaustive error | Missing enum variant | Add arm |
| Wildcard hides new variants | _ arm absorbs them | Name variants explicitly |
| Guards don’t count | Compiler ignores guard | Add wildcard or explicit arm |
| Unreachable pattern | Earlier arm catches all | Reorder arms |
| Range gaps | Ranges don’t cover type | Fill the gaps |
non_exhaustive missing wildcard | Cross-crate enum | Add _ arm |
| Unreachable binding after wildcard | Wildcard first | Move wildcard last |
Real-World Examples
1. Full Enum Coverage
match status {
Status::Active => { }
Status::Inactive => { }
Status::Pending => { }
}
2. Wildcard for Numbers
match score {
0 => "zero",
n if n > 0 => "positive",
_ => "negative",
}
3. Range Coverage
match age {
0..=12 => "child",
13..=19 => "teen",
20..=64 => "adult",
_ => "senior",
}
4. Tuple Coverage
match (a, b) {
(true, true) => { }
(true, false) => { }
(false, true) => { }
(false, false) => { }
}
5. Nested Enum
match result {
Ok(Some(x)) => { }
Ok(None) => { }
Err(e) => { }
}
6. Non-Exhaustive Library Enum
match lib_enum {
LibEnum::Known => { }
_ => { }
}
7. Option and Result
match value {
Some(n) => { }
None => { }
}
8. Guard with Fallback
match n {
x if x < 0 => "neg",
0 => "zero",
_ => "pos",
}
9. Or Pattern Coverage
match c {
'a' | 'e' | 'i' | 'o' | 'u' => "vowel",
_ => "consonant",
}
10. Struct Field Coverage
match point {
Point { x: 0, y: 0 } => "origin",
Point { x: 0, .. } => "y-axis",
Point { y: 0, .. } => "x-axis",
Point { .. } => "general",
}
Visual
Exhaustiveness Check
┌─────────────────────────────────────────────────────────────┐
│ enum Color { Red, Green, Blue } │
│ │
│ match c { │
│ Color::Red => { } │
│ Color::Green => { } │
│ } │
│ │
│ Compiler analysis: │
│ Possible values: Red, Green, Blue │
│ Covered: Red, Green │
│ Missing: Blue │
│ Result: COMPILE ERROR │
│ │
│ match c { │
│ Color::Red => { } │
│ Color::Green => { } │
│ Color::Blue => { } │
│ } │
│ │
│ Covered: Red, Green, Blue │
│ Result: OK │
│ │
└─────────────────────────────────────────────────────────────┘
Wildcard Effect on Refactoring
┌─────────────────────────────────────────────────────────────┐
│ WITHOUT WILDCARD │
│ │
│ enum Status { Active, Inactive } │
│ │
│ match s { │
│ Status::Active => { } │
│ Status::Inactive => { } │
│ } │
│ │
│ Add variant: Status::Pending │
│ → COMPILE ERROR: missing Pending │
│ → Compiler points to every match │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ WITH WILDCARD │
│ │
│ match s { │
│ Status::Active => { } │
│ _ => { } │
│ } │
│ │
│ Add variant: Status::Pending │
│ → NO ERROR: Pending falls into _ │
│ → Behavior may be wrong, silently │
│ │
│ Wildcard trades safety for convenience. │
│ │
└─────────────────────────────────────────────────────────────┘
Guards and Coverage
┌─────────────────────────────────────────────────────────────┐
│ GUARD DOES NOT CONTRIBUTE TO COVERAGE │
│ │
│ match n { │
│ x if x > 0 => "positive", │
│ x if x < 0 => "negative", │
│ } │
│ │
│ Compiler analysis: │
│ - Arm 1 covers: nothing guaranteed (guard may be false) │
│ - Arm 2 covers: nothing guaranteed │
│ - Missing: everything (0, and any value where guards fail) │
│ Result: COMPILE ERROR │
│ │
│ match n { │
│ x if x > 0 => "positive", │
│ x if x < 0 => "negative", │
│ 0 => "zero", │
│ } │
│ │
│ The explicit 0 arm covers the remaining case. │
│ Result: OK (in this specific case) │
│ │
└─────────────────────────────────────────────────────────────┘
non_exhaustive Across Crates
┌─────────────────────────────────────────────────────────────┐
│ LIBRARY CRATE │
│ │
│ #[non_exhaustive] │
│ pub enum ApiError { │
│ NotFound, │
│ Unauthorized, │
│ } │
│ │
│ Library can add variants in future versions. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ DOWNSTREAM CRATE │
│ │
│ match err { │
│ ApiError::NotFound => { } │
│ ApiError::Unauthorized => { } │
│ _ => { } // REQUIRED │
│ } │
│ │
│ The wildcard handles future variants. │
│ Without it: compile error. │
│ │
│ Trade-off: library evolves freely, │
│ downstream must handle unknown cases. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Exhaustiveness | Every possible value covered |
| Enforcement | Compile-time error |
| Enum coverage | Arm per variant or wildcard |
| Bool coverage | Arms for true and false |
| Range coverage | Ranges must cover the type |
| Guard | Does not contribute to coverage |
| Wildcard | Always exhaustive |
non_exhaustive | Requires wildcard across crates |
| Unreachable pattern | Warning, not error |
| Recommended | Name all variants explicitly |
Key takeaways:
- Exhaustiveness is a compile-time guarantee. A
matchmust cover every possible value of the matched type. The compiler proves this by analyzing patterns, and a non-exhaustive match is a compile error. - Enum matches need an arm per variant. For an enum with three variants, a match with three arms is exhaustive. Missing one is an error. A wildcard can substitute for the remaining arms.
- Guards do not contribute to coverage. An arm with a guard may not execute even if the pattern matches, because the guard can be false. The compiler does not count guarded arms toward exhaustiveness.
- The wildcard makes any match exhaustive.
_matches everything, so a match with a wildcard arm is always exhaustive. But the wildcard hides new variants from the compiler. - Naming variants explicitly is the point. When you name every variant instead of using
_, adding a new variant produces a compile error at every incomplete match. This is the refactoring aid that exhaustiveness provides. non_exhaustivechanges the rules across crates. An enum marked#[non_exhaustive]may gain variants in future versions. Downstream crates must include a wildcard arm to handle unknown variants.- Ranges must cover the type. A match on a
u8with ranges that leave a gap is non-exhaustive. The compiler reports the missing range. - Unreachable patterns are warnings. A pattern after a wildcard or after a broader pattern is unreachable. The compiler warns, and the pattern is dead code.
Remember: Exhaustiveness is not a restriction. It is a guarantee that no case is forgotten. In languages with partial matching, adding a new case is a manual refactoring task—find every place that handles the type and update it. In Rust, the compiler does the finding. A non-exhaustive match is a compile error, and the error message names the missing case. This turns refactoring from a search problem into a checklist. The wildcard is the escape hatch, but it should be used deliberately. On an exhaustive enum, naming every variant gives the compiler the information it needs to catch future changes. On an infinite type like i32, the wildcard is necessary. Know which situation you are in, and choose the pattern that makes the compiler your ally.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!