What is exhaustive matching

Exhaustive matching is a Rust compiler rule requiring all enum variants to be handled in a match expression to prevent unhandled cases.

When the compiler refuses to guess

You are building a simple game engine. You define an enum for player actions: Jump, Crouch, and Shoot. You write a function to process the input. You handle Jump and Crouch. You forget Shoot. In JavaScript, the function returns undefined and your character freezes. In Python, you might hit an unhandled exception three layers deep. In Rust, the build fails before you can run a single frame.

The compiler stops you with a precise error. It tells you exactly which variant you missed. This is exhaustive matching. It is not a suggestion. It is a structural guarantee that every possible value flowing through your code has a designated path.

The concept in plain words

Rust treats enums as closed sets. When you define an enum, you draw a circle around every possible state that value can take. The match expression is the gatekeeper. It demands that you provide a handler for every single item inside that circle. If you leave a gap, the compiler considers the code incomplete.

Think of a physical switchboard with labeled slots. Each slot corresponds to a variant. When a signal arrives, it must click into one of those slots. There is no unmarked drawer where stray signals go to disappear. The system is designed so that every input has a known destination. Rust enforces this at compile time. You cannot ship code that leaves a slot empty.

This design choice eliminates entire classes of runtime bugs. You never get a case not handled crash in production. You never accidentally ignore a new error variant because you copied an old match block and forgot to update it. The compiler forces you to confront every possibility before the code runs.

The compiler does not want you to guess. It wants you to decide.

A minimal example

Here is the baseline pattern. The enum defines two states. The match expression handles both.

/// Represents the version of an IP address.
enum IpVersion {
    V4,
    V6,
}

/// Routes traffic based on the IP version.
fn route(version: IpVersion) {
    match version {
        // Handle IPv4 traffic through the legacy gateway.
        IpVersion::V4 => println!("Routing through IPv4 gateway"),
        // Handle IPv6 traffic through the modern backbone.
        IpVersion::V6 => println!("Routing through IPv6 backbone"),
    }
}

If you delete the V6 arm, the compiler rejects the file. It does not guess that you meant to ignore it. It does not assume a default behavior. It demands explicit coverage.

What happens under the hood

The exhaustiveness check is purely a compile-time operation. The compiler performs structural analysis on your enum definition. It builds a complete decision tree of all possible variants. It then compares your match arms against that tree. If every leaf in the tree has a corresponding arm, the check passes. If a leaf is missing, the compiler emits E0004 (non-exhaustive patterns).

At runtime, there is zero overhead for this guarantee. The match expression compiles down to machine code. For simple enums with contiguous discriminants, the compiler often generates a jump table. For enums with data or sparse variants, it compiles to a series of conditional branches. The exhaustiveness check never touches the running program. It is a static proof that your control flow is complete.

Zero runtime cost for a compile-time safety net. That is the deal.

Real-world usage

Exhaustive matching shines when you are building systems that evolve. Consider a configuration parser that reads settings from a file. You define an enum for the possible setting types.

/// Possible types of configuration values.
enum ConfigValue {
    Boolean(bool),
    Integer(i32),
    String(String),
    List(Vec<String>),
}

/// Converts a configuration value to a display string.
fn format_value(value: ConfigValue) -> String {
    match value {
        // Booleans render as uppercase for clarity.
        ConfigValue::Boolean(b) => b.to_string().to_uppercase(),
        // Integers pass through directly.
        ConfigValue::Integer(n) => n.to_string(),
        // Strings are wrapped in quotes to show boundaries.
        ConfigValue::String(s) => format!("\"{}\"", s),
        // Lists join with commas and wrap in brackets.
        ConfigValue::List(items) => format!("[{}]", items.join(", ")),
    }
}

When the product team adds a Float(f64) variant next month, every match on ConfigValue in your codebase breaks. The compiler points you to every location that needs updating. You cannot accidentally leave a legacy function handling only three types while the system now supports four. The breakage is immediate and localized.

Community convention treats this breakage as a feature. Breaking changes in enums force downstream code to acknowledge new states. It prevents silent data loss. When you add a variant, you want every consumer to think about how to handle it. The compiler makes that unavoidable.

Pitfalls and compiler errors

The most common friction point is the wildcard pattern _. The _ arm matches anything that has not been matched yet. It satisfies the exhaustiveness check instantly.

fn handle_command(cmd: String) {
    match cmd.as_str() {
        "exit" => println!("Shutting down"),
        // Catches every other command without naming them.
        _ => println!("Unknown command"),
    }
}

This compiles fine. It also hides bugs. If you add a new "restart" command tomorrow, the _ arm swallows it. You will not know until a user tries it. Use the wildcard only when you genuinely do not care about the remaining cases. Treat it as a deliberate design choice, not a shortcut.

Library authors face a different problem. If you publish a crate with an enum, downstream users will write match expressions that cover your current variants. When you add a new variant in a minor version, you break their builds. The #[non_exhaustive] attribute solves this.

/// An error type that may grow over time.
#[non_exhaustive]
pub enum NetworkError {
    Timeout,
    ConnectionRefused,
}

When you mark an enum as non-exhaustive, the compiler forces downstream code to use _ or .. for that type. It tells the compiler that more variants might appear later. This lets you expand the enum without breaking API consumers. You trade compile-time exhaustiveness for forward compatibility.

The compiler error E0004 will appear if you forget a variant or if you try to match a non-exhaustive enum without a fallback. Read the error carefully. It lists the missing patterns and often suggests adding a wildcard. Follow the suggestion only if you are sure you want to ignore future variants.

Treat the wildcard like a safety net, not a trash can.

When to use match vs alternatives

Rust gives you several ways to handle conditional logic. The right choice depends on how many cases you expect and how strictly you want the compiler to enforce coverage.

Use match when you need to handle every possible variant of an enum or pattern. Use match when the logic for each branch is substantial and you want the compiler to verify completeness. Use match when you are destructuring complex data and need to bind variables from each variant.

Reach for if let when you only care about one specific variant and want to ignore the rest. Reach for if let when the alternative would require a _ => {} arm that adds noise. Reach for if let when you are chaining multiple optional or single-variant checks in a linear flow.

Pick if/else chains when you are working with primitive types like integers or booleans. Pick if/else when the conditions are ranges or complex boolean expressions rather than discrete enum variants. Pick if/else when you want the compiler to allow gaps without warning.

Use #[non_exhaustive] when you are publishing a library and plan to add enum variants later. Use #[non_exhaustive] when you want to force downstream code to handle unknown cases gracefully. Use #[non_exhaustive] when backward compatibility matters more than strict compile-time coverage.

Pick the tool that matches your certainty.

Where to go next