| |

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

TypeExhaustive Match Requires
boolArms for true and false
Enum (exhaustive)Arm for every variant
Enum (non_exhaustive)Arm for every variant + wildcard
u8Ranges covering 0..=255 or wildcard
i32Wildcard or binding
TupleAll combinations of component values
ReferenceSame as underlying type

Pattern Coverage

PatternCovers
LiteralThe exact value
RangeAll values in range
OrUnion of alternatives
Enum variantThat variant
BindingEverything
Wildcard _Everything
GuardNothing (guards do not contribute)

Common Compiler Errors

ErrorCauseFix
non-exhaustive patternsMissing variant or valueAdd arm or wildcard
unreachable patternEarlier arm catches allRemove or reorder
unreachable pattern with guardGuard always trueRemove guard or reorder
Missing wildcardnon_exhaustive enumAdd _ arm

The Wildcard Trade-off

Use WildcardAvoid Wildcard
Infinite types (i32)Exhaustive enums
non_exhaustive enumsInternal enums
Genuinely shared behaviorCases that should be explicit
PrototypingProduction 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

PitfallWhy It HappensFix
Non-exhaustive errorMissing enum variantAdd arm
Wildcard hides new variants_ arm absorbs themName variants explicitly
Guards don’t countCompiler ignores guardAdd wildcard or explicit arm
Unreachable patternEarlier arm catches allReorder arms
Range gapsRanges don’t cover typeFill the gaps
non_exhaustive missing wildcardCross-crate enumAdd _ arm
Unreachable binding after wildcardWildcard firstMove 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

ItemValue
ExhaustivenessEvery possible value covered
EnforcementCompile-time error
Enum coverageArm per variant or wildcard
Bool coverageArms for true and false
Range coverageRanges must cover the type
GuardDoes not contribute to coverage
WildcardAlways exhaustive
non_exhaustiveRequires wildcard across crates
Unreachable patternWarning, not error
RecommendedName all variants explicitly

Key takeaways:

  • Exhaustiveness is a compile-time guarantee. A match must 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_exhaustive changes 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 u8 with 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!