The _ Wildcard and Exhaustiveness

Rust’s match expression is one of the language’s most powerful tools, but it comes with a strict rule: every possible value of the type being matched must be handled. The _ wildcard pattern is how you tell the compiler “match anything I haven’t explicitly listed.” Understanding _ and the exhaustiveness rule it satisfies is essential, because Rust will refuse to compile a match that leaves any case unhandled — this is one of the compiler’s strongest guarantees against forgotten edge cases.

Overview / How It Works

In many languages, a switch statement is allowed to be incomplete: if none of the cases match, execution just falls through and nothing happens (or a default runtime behavior kicks in). Rust does not allow this for match. The compiler performs exhaustiveness checking: it analyzes every arm’s pattern and proves, at compile time, that the set of patterns covers every value the matched type could possibly hold. If it cannot prove that, compilation fails with an error like non-exhaustive patterns.

Think of it like a lawyer building a case: the compiler must be convinced, beyond any doubt, that there is no possible input value that would fall through without being caught by an arm. For a type like bool, that means every match must eventually cover both true and false. For an enum with five variants, every variant must be covered. For a type with an enormous domain — an i32, a u16, a String — listing every possible value is not practical, so you need a catch-all. That catch-all is the _ wildcard pattern: it matches any value and, critically, does not bind that value to a variable.

This is different from a named catch-all like other, which also matches anything but gives you a variable you can use inside the arm. _ matches anything and discards it — you cannot refer to the value it matched. This distinction matters more than it looks: reaching for other instead of _ is often the right move when the arm’s logic needs the unmatched value.

Exhaustiveness checking is a compile-time-only safety net. It costs nothing at runtime, but it prevents an entire category of bug: the “I forgot to handle that case” bug that, in other languages, silently does nothing or crashes in production. When you add a new variant to an enum later, every match on that enum that does not already use _ will fail to compile until you explicitly decide what the new variant should do — this is a feature, not friction.

Syntax

The wildcard pattern appears as the last arm of a match, after every specific pattern you care about:

match VALUE {
    PATTERN1 => EXPR1,
    PATTERN2 => EXPR2,
    _ => DEFAULT_EXPR,
}
  • VALUE — the expression being matched; its type determines what must be exhaustively covered.
  • PATTERN1, PATTERN2, … — specific patterns (literals, enum variants, ranges, tuples, etc.) checked top to bottom; the first one that matches wins.
  • _ — the wildcard; matches any remaining value that no earlier pattern matched. It must come last, because patterns are tried in order and _ would otherwise shadow everything after it.
  • DEFAULT_EXPR — the expression run when _ matches; since _ binds nothing, this expression cannot reference the matched value.

The _ pattern is not exclusive to match — it appears anywhere Rust expects a pattern: function parameters, let bindings, and tuple/struct destructuring, wherever you want to accept or destructure a value without naming a part of it.

Examples

The first example matches a small, fully-enumerable set of integers and falls back to _ for everything else:

fn main() {
    let day = 3;

    match day {
        1 => println!("Monday"),
        2 => println!("Tuesday"),
        3 => println!("Wednesday"),
        4 => println!("Thursday"),
        5 => println!("Friday"),
        _ => println!("Weekend"),
    }
}

Output:

Wednesday

Without the final _ => ... arm, this would not compile: day is an i32, and an i32 can hold billions of values the five listed arms do not cover. The wildcard is what makes the match exhaustive.

The second example matches against an enum. Here _ is used to group several variants under one shared behavior instead of writing them out individually:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn action(light: &TrafficLight) -> &str {
    match light {
        TrafficLight::Red => "Stop",
        TrafficLight::Green => "Go",
        _ => "Slow down",
    }
}

fn main() {
    let lights = [TrafficLight::Red, TrafficLight::Yellow, TrafficLight::Green];

    for light in &lights {
        println!("{}", action(light));
    }
}

Output:

Stop
Slow down
Go

TrafficLight has three variants. The match explicitly names Red and Green, and _ silently absorbs Yellow (and, notably, would absorb any future variant added to the enum without warning — more on why that matters in Common Mistakes).

The third example is closer to real code: categorizing HTTP-style status codes using inclusive range patterns, with _ covering everything outside the known ranges:

fn describe_status(code: u16) -> &'static str {
    match code {
        200..=299 => "Success",
        300..=399 => "Redirection",
        400..=499 => "Client Error",
        500..=599 => "Server Error",
        _ => "Unknown Status",
    }
}

fn main() {
    let codes: [u16; 5] = [200, 301, 404, 500, 999];

    for code in codes {
        println!("{}: {}", code, describe_status(code));
    }
}

Output:

200: Success
301: Redirection
404: Client Error
500: Server Error
999: Unknown Status

A u16 can hold 65,536 distinct values. Listing them all would be absurd, and the four range patterns only cover 200–599. The _ arm is what makes this exhaustive over the full u16 domain, catching values like 999 that fall outside every named range.

The _ Pattern Beyond match

_ also shows up outside match, anywhere Rust expects you to bind a value but you have no use for part of it:

fn main() {
    let (x, _, z) = (1, 2, 3);
    println!("{} {}", x, z);
}

Output:

1 3

Here _ ignores the middle element of the tuple during destructuring. The same idea applies to function parameters you must declare but never use inside the body:

fn always_zero(_input: i32) -> i32 {
    0
}

fn main() {
    println!("{}", always_zero(42));
}

Output:

0

Note the underscore is placed as a prefix on the parameter name (_input), not a bare _, so it still reads clearly at the call site while suppressing the “unused variable” warning.

How the Compiler Checks Exhaustiveness

When the compiler encounters a match, it does the following, conceptually:

  • It determines the full set of values the matched expression’s type can hold — for a bool that is {true, false}; for an enum it is the set of variants; for an integer type it is every representable number.
  • It walks through the arms in order and subtracts each pattern’s covered values from the remaining “uncovered” set.
  • If, after processing every arm, the uncovered set is empty, the match is exhaustive and compiles. If any value is still uncovered, compilation fails with a non-exhaustive patterns error naming an example value that is not handled.
  • The _ pattern is treated specially: whatever remains uncovered when the compiler reaches it, _ covers all of it. Placing _ means “I accept responsibility for everything not explicitly named above.”
  • Because patterns are checked top-to-bottom and earlier arms consume their matched values first, a _ arm placed before other arms would make those later arms unreachable — the compiler warns about this as dead code.

This is also why matching every enum variant explicitly (no _) is often preferable when feasible: the compiler will force you to revisit every existing match on that enum the moment a new variant is added, turning a potential bug into a compile error you fix immediately.

Common Mistakes

Mistake 1: Forgetting the wildcard on a match over a large type. Beginners often list a few cases and expect the rest to be implicitly ignored, the way a plain if/else if chain might silently do nothing.

fn main() {
    let n = 5;
    match n {
        1 => println!("one"),
        2 => println!("two"),
    }
}

This fails to compile because n is an i32, and only two of its billions of possible values are handled. The compiler reports a non-exhaustive patterns error and refuses to build. The fix is to add a wildcard (or a named catch-all) arm:

let n = 5;
match n {
    1 => println!("one"),
    2 => println!("two"),
    _ => println!("something else"),
}

Output:

something else

Mistake 2: Trying to use the value that _ matched. Because _ does not bind a name, you cannot refer to it inside the arm’s expression — this is a common trap for anyone assuming _ behaves like a variable named underscore.

fn main() {
    let code = 7;
    match code {
        1 => println!("one"),
        2 => println!("two"),
        _ => println!("unexpected: {}", _),
    }
}

This does not compile: _ is a pattern, not an expression, and the compiler rejects using it as a value inside println!. The fix is to use a named catch-all pattern instead of _ whenever you need the matched value:

let code = 7;
match code {
    1 => println!("one"),
    2 => println!("two"),
    other => println!("unexpected: {}", other),
}

Output:

unexpected: 7

A bare identifier like other is itself a pattern that matches anything and binds it — it plays the same exhaustiveness role as _, but gives you access to the value.

Mistake 3: Using _ on an enum and forgetting it hides future variants. This one will not stop your code from compiling, which is exactly what makes it dangerous. Recall the TrafficLight example: if a teammate later adds a FlashingRed variant to the enum, the existing match with a _ arm keeps compiling and silently treats the new variant as “Slow down” — even if that is the wrong behavior for a flashing red light. Had the match listed every variant explicitly with no _, adding the new variant would force a compile error at every call site that needs updating. As a rule of thumb, reach for _ when you genuinely want a shared fallback for many cases (like the HTTP status ranges), and list variants explicitly when each one deserves its own decision.

Best Practices

  • Prefer listing every enum variant explicitly over using _ when each variant needs distinct handling — you want the compiler to flag the match again when a new variant is added.
  • Use _ when you deliberately want a single shared fallback behavior for many values, such as default cases for large numeric ranges or truly “don’t care” variants.
  • Reach for a named catch-all pattern (like other) instead of _ whenever the fallback arm needs to read the value that didn’t match anything else.
  • Prefix intentionally unused function parameters and destructured bindings with an underscore (_config) rather than a bare _ when the name still adds clarity at the call site.
  • Always place the _ arm last — patterns are checked top to bottom, and anything after _ is unreachable dead code.
  • Let the compiler’s exhaustiveness errors guide you: when it names an uncovered case, that is often a real gap in your logic, not just a formality to silence.

Practice Exercises

  • Write a function grade_letter(score: u8) -> char that returns 'A' for 90–100, 'B' for 80–89, 'C' for 70–79, and 'F' for anything else, using range patterns and _. Test it with a few scores including one outside the letter ranges (like 45) and print the results.
  • Define an enum Shape { Circle, Square, Triangle, Hexagon } and write a match that prints the number of sides for each variant, matching every variant explicitly (no _). Then add a new variant, Pentagon, and observe (mentally or by trying it) that the existing match now fails to compile until you handle it.
  • Write a small program that destructures a 4-tuple (i32, i32, i32, i32), keeping only the first and last values with _ for the middle two, and prints them. Then rewrite it using a named catch-all binding on the second element instead of _, and print that value too.

Summary

  • Every match in Rust must be exhaustive: the compiler proves at compile time that every possible value of the matched type is covered by some arm.
  • The _ wildcard pattern matches any remaining value and is the standard way to satisfy exhaustiveness for large or open-ended types.
  • _ does not bind a value — you cannot reference what it matched; use a named catch-all pattern like other when you need the value.
  • _ also appears outside match, in tuple/struct destructuring and function parameters, to accept or ignore a value without naming it.
  • Using _ on an enum silently absorbs any future variants added to that enum, which can hide bugs; prefer listing variants explicitly when each needs distinct handling.
  • The _ arm must come last in a match, since earlier arms are checked first and anything placed after _ is unreachable.