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
boolthat is{true, false}; for anenumit 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 patternserror 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
enumvariant 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) -> charthat 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 (like45) and print the results. - Define an
enum Shape { Circle, Square, Triangle, Hexagon }and write amatchthat 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
matchin 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 likeotherwhen you need the value._also appears outsidematch, in tuple/struct destructuring and function parameters, to accept or ignore a value without naming it.- Using
_on anenumsilently 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 amatch, since earlier arms are checked first and anything placed after_is unreachable.
