Fix E0023: Non-Exhaustive Pattern Matching in Rust

Rust intermediate Linux macOS Windows WebAssembly

1. Symptoms

The Rust compiler emits E0023 when a pattern match does not cover all possible values of a type. This error appears in several distinct scenarios, each producing characteristic diagnostic output.

Incomplete Match Expression

When using a match statement on an enum without covering all variants, the compiler reports:

error[E0023]: pattern None not covered –> src/main.rs:5:5 | 5 | match opt { | ^ non-exhaustive patterns: None not covered | note: Option<i32> defined here (1 variant) –> /rustc/…/library/core/src/option.rs:… | 1 | enum Option { /* … */ } | ^^^^^^^^^^^^^^ None defined here | help: consider adding a wildcard arm | 5 | match opt { 6 | Some(x) => println!("{}", x), 7 | _ => {} // handle remaining cases 8 | } |


### Conditional Expression Without Exhaustive Coverage

When using `if let` without a corresponding `else` clause:

error[E0023]: if let requires exhaustive patterns due to the current semantics of if let –> src/main.rs:6:4 | 6 | if let Some(x) = opt { | ^^^^ pattern None not covered | help: consider adding an else block | 6 | if let Some(x) = opt { /* … / } else { / … */ }


### Non-Exhaustive Enum from External Crate

When matching against an enum marked with `#[non_exhaustive]` from the standard library or another crate:

error[E0023]: pattern _ not covered –> src/main.rs:8:9 | 8 | match err.kind() { | ^ non-exhaustive patterns: ErrorKind has more variants than currently matched (255 not covered) | note: variant Other and 255 more fields not covered (255 not covered) –> /rustc/…/library/std/src/io/error.rs:… |


### Implicit Unreachable Pattern

When compiler analysis determines that certain patterns can never match:

error[E0023]: this pattern is intentionally unreachable –> src/main.rs:9:21 | 9 | match x { | ^ pattern None cannot match Some pattern | help: consider removing the unreachable pattern | 9 | None => unreachable!(), | ^^^^^^^^^^^^^^^^^^^^^^^^


## 2. Root Cause

The fundamental mechanism behind E0023 stems from Rust's exhaustiveness checking, a compile-time analysis that ensures pattern matches cover all possible values. This guarantee is central to Rust's safety guarantees: if the compiler can prove that a pattern match might not handle all cases, it rejects the code rather than allowing undefined behavior at runtime.

The exhaustiveness checker operates by analyzing the set of patterns provided against the type being matched. For enums, each variant represents a disjoint set of possible values, and the checker verifies that the union of all pattern sets equals the complete set of values. When a gap exists, E0023 emerges.

In the case of conditional expressions like `if let`, Rust applies the same exhaustiveness semantics differently. Unlike traditional `if` statements that evaluate to `bool`, `if let` expressions preserve the semantic of "does this pattern match this value?" The compiler requires that this check be exhaustive because the alternative case has semantic meaning—if the pattern doesn't match, we must take the `else` branch. Without an explicit `else`, the compiler cannot guarantee well-defined behavior.

For enums marked `#[non_exhaustive]` from external crates, the situation differs fundamentally. The compiler knows that the defining crate may add new variants in future versions without breaking API compatibility. Because you cannot enumerate all future variants, the compiler requires a catch-all pattern (`_`) to handle any additional variants that may exist in newer versions of the crate. This design protects your code from silent breakage when dependencies update.

The pattern matching algorithm in Rust uses structural analysis of types. For simple enums, the checker examines each variant. For enums with associated data, it ensures either explicit handling of each variant or a wildcard pattern. For types with unknown future variants (external non-exhaustive enums), only wildcard patterns provide exhaustiveness.

## 3. Step-by-Step Fix

### Fix: Complete Enum Match with Explicit Variants

The most explicit solution adds handling for every enum variant directly:

**Before:**
```rust
fn process_option(opt: Option<i32>) {
    match opt {
        Some(x) => println!("Got: {}", x),
        // None variant not handled - compiler error
    }
}

After:

fn process_option(opt: Option<i32>) {
    match opt {
        Some(x) => println!("Got: {}", x),
        None => println!("Got nothing"),
    }
}

Fix: Wildcard Pattern for Future Variants

When you intentionally want to ignore unhandled cases, use the wildcard pattern:

Before:

fn handle_error(kind: std::io::ErrorKind) -> &'static str {
    match kind {
        std::io::ErrorKind::NotFound => "File not found",
        std::io::ErrorKind::PermissionDenied => "Access denied",
        // More variants exist, compiler requires handling
    }
}

After:

fn handle_error(kind: std::io::ErrorKind) -> &'static str {
    match kind {
        std::io::ErrorKind::NotFound => "File not found",
        std::io::ErrorKind::PermissionDenied => "Access denied",
        std::io::ErrorKind::AlreadyExists => "Already exists",
        _ => "Unknown error",
    }
}

Fix: Conditional Expression with Else Branch

Convert if let to a complete conditional by adding the else case:

Before:

fn check_value(opt: Option<i32>) {
    if let Some(x) = opt {
        println!("Found: {}", x);
    }
    // Missing else - incomplete pattern
}

After:

fn check_value(opt: Option<i32>) {
    if let Some(x) = opt {
        println!("Found: {}", x);
    } else {
        println!("Not found");
    }
}

Alternatively, use a wildcard pattern:

After (alternative):

fn check_value(opt: Option<i32>) {
    if let Some(x) = opt {
        println!("Found: {}", x);
    } else if opt.is_none() {
        println!("Was None");
    }
}

Fix: Guarded Pattern with Wildcard

When only certain variants need special handling:

Before:

enum Status {
    Pending,
    Processing,
    Complete,
    Failed(String),
}

fn handle_status(status: Status) -> &'static str {
    match status {
        Status::Pending => "Waiting",
        Status::Processing => "In progress",
        // Incomplete - missing Complete and Failed
    }
}

After:

fn handle_status(status: Status) -> &'static str {
    match status {
        Status::Pending => "Waiting",
        Status::Processing => "In progress",
        Status::Complete => "Done",
        Status::Failed(e) => {
            println!("Error: {}", e);
            "Failed"
        }
    }
}

Fix: Nested Enum Exhaustiveness

Deeply nested patterns require checking all levels:

Before:

fn process_nested(opt: Option<Result<i32, String>>) {
    match opt {
        Some(Ok(v)) => println!("Success: {}", v),
        Some(Err(e)) => println!("Error: {}", e),
        // None not handled
    }
}

After:

fn process_nested(opt: Option<Result<i32, String>>) {
    match opt {
        Some(Ok(v)) => println!("Success: {}", v),
        Some(Err(e)) => println!("Error: {}", e),
        None => println!("Option was None"),
    }
}

4. Verification

After applying the fix, verify the code compiles correctly by running cargo build or cargo check. The E0023 error should disappear, and the compiler should succeed without warnings about incomplete patterns.

Create a test case that exercises each branch of your pattern match to ensure runtime behavior matches the static guarantees:

#[test]
fn test_option_handling() {
    let some_value = Some(42);
    let none_value: Option<i32> = None;
    
    let result_some = match some_value {
        Some(x) => x * 2,
        None => 0,
    };
    assert_eq!(result_some, 84);
    
    let result_none = match none_value {
        Some(x) => x * 2,
        None => 0,
    };
    assert_eq!(result_none, 0);
}

For external non-exhaustive enums, verify that your catch-all pattern handles unexpected variants gracefully by checking the behavior when a new variant might be introduced. Since you cannot directly test future variants, review your code to ensure the wildcard arm provides reasonable default behavior.

Run cargo clippy to identify any additional exhaustiveness warnings that might indicate similar patterns elsewhere in your codebase:

cargo clippy -- -W clippy::pattern-matching

The compiler’s help message often suggests the minimal change required, such as adding _ => {} or an else block. After fixing, ensure the behavior remains consistent with your original intent.

5. Common Pitfalls

The primary pitfall involves assuming that a single variant handles all cases. Many developers new to Rust expect Some(x) to match all Option values, but the enum definition explicitly includes None. The compiler’s exhaustive checking prevents this common oversight.

Another frequent mistake occurs when updating enums without updating corresponding match expressions. When adding a new variant to an enum, every match expression on that enum requires updating. Using wildcard patterns (_) can provide forward compatibility, but ensure the wildcard behavior is intentional rather than accidentally swallowing cases that should be handled explicitly.

With if let, developers often write code expecting the else branch to implicitly return from the current scope. This is not the case—Rust requires explicit handling of all patterns. The following code is incorrect:

// WRONG - does not compile
let x = if let Some(v) = opt { v }  // missing else

The correct approach explicitly handles the alternative case:

// CORRECT
let x = if let Some(v) = opt { v } else { 0 };

For non-exhaustive enums from external crates, forgetting the wildcard pattern leads to compilation failure on dependency updates. Always include _ => {} or a similar catch-all when matching against standard library non-exhaustive enums like ErrorKind, SocketAddr, or any enum marked with #[non_exhaustive].

Nested patterns can silently introduce non-exhaustiveness. A pattern like Some(Some(_)) covers only one of the four possible combinations of nested Option. Always verify that your patterns cover all combinations when dealing with nested types.

Finally, be cautious with conditional patterns that involve or-patterns (|). The pattern Some(1) | Some(2) covers only those specific values, not all Some variants. This can be surprising when the enum has many variants or when the enum is later extended.

E0004: Pattern not covered in match — This error specifically indicates that a particular pattern arm does not match any possible value of its type. While E0023 covers the overall non-exhaustiveness of the match, E0004 highlights specific pattern issues like unreachable code or impossible matches. The distinction matters: E0023 requires adding patterns, while E0004 may require removing or correcting existing patterns.

E0026: Pattern does not mention fields of variant — This error occurs when matching on a struct-like enum variant without specifying all its fields. When an enum variant carries named or unnamed data, pattern matching must either destructure all fields or use .. to ignore remaining fields. E0023 can occur alongside E0026 when the overall match is incomplete, but E0026 specifically addresses the structural completeness of individual variant patterns.

E0530: match/if let escapes on a type that doesn’t allow it — This error arises when attempting to match against a type that cannot be pattern matched, such as function pointers or references with unknown referent lifetime. While E0023 deals with incomplete coverage of matchable types, E0530 indicates that the type itself cannot be used in pattern matching contexts. The fix differs fundamentally: E0530 requires changing your approach entirely, while E0023 only requires adding missing patterns.