RFA-441 · Case file with fixtures · Case 413 of 694 · Compiler evidence
A Plain let Binding Requires an Irrefutable Pattern
Plain let patterns must succeed for every value of the expression type. Use let-else for an early exit, if let for optional work, match for distinct outcomes, or an irrefutable destructure when failure is impossible by type.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Some is a refutable Option pattern, while an ordinary let statement requires a pattern that succeeds for every possible value of its expression.
- First discriminating check
- Use let-else for required presence, if let for optional work, match for distinct outcomes, or change the type when absence is impossible.
I knew an Option was usually present and tried to destructure it with let Some(number) = value. Rust reported E0005 because None was still a valid value and the statement supplied no path for it.
The failing fixture deliberately sets the option to None, but the compiler would reject the binding even if it were currently constructed as Some. The declared type admits both variants.
Irrefutable means the pattern cannot fail
The pattern Reference calls a pattern irrefutable when it matches every possible value of the type.
These are irrefutable:
let name = string;
let (left, right) = pair;
let Single::Value(item) = single_variant;
Some(number) is refutable because it rejects None. A literal, range, or one variant of a multi-variant enum is normally refutable too.
let-else fits required presence with an early exit
The repaired fixture uses:
let Some(number) = value else {
return None;
};
The else branch diverges from the current control-flow path, so after the statement number is definitely bound. This is useful for validation pipelines where failure returns, breaks, continues, or panics according to an explicit policy.
The Atlas has a separate case showing why a let-else branch must diverge. Together, the rules ensure bindings exist on every path that continues.
if let fits optional work
When absence should simply skip a block, I use:
if let Some(number) = value {
process(number);
}
The binding exists only inside the block. An else can handle absence, but results from several branches may read more clearly as a match.
I avoid using if-let when ignoring None would hide missing required data.
match fits several meaningful outcomes
match is best when Some and None produce different values, side effects, or errors. Its exhaustiveness check makes both policies visible.
For Result, matching Ok and Err preserves error context. Replacing a refutable binding with .unwrap() changes compile-time incompleteness into a runtime panic and often loses domain handling.
I use ? when propagation is exactly the intended error path, not only to reduce indentation.
“I already checked” needs structural evidence
Code sometimes checks value.is_some() and then tries a plain Some binding. The boolean fact is not carried as a refinement of the variable's type across arbitrary statements. Mutation, aliasing, or refactoring could separate the check from the use.
Combining the check and extraction with match, let-else, or if let makes the relationship atomic in source. Methods such as as_ref, take, and ok_or can also express ownership choices directly.
Function parameters also require irrefutable patterns
Patterns in parameters must accept every argument of the declared type. A function cannot declare that it takes Option<T> and then have no entry behaviour for None.
The caller can pass a narrower type instead, such as T, if presence is an API precondition enforced before the call. This often produces cleaner boundaries: parse or validate once, then pass a type representing success.
Single-variant types can encode certainty
If a state truly has only one possible structural form, a one-variant enum or struct destructure is irrefutable. More commonly, a newtype wraps validated data so later code does not repeatedly handle invalid states.
I do not create types only to avoid one match, but in larger systems they can move validation to a clear boundary.
My E0005 checklist
I choose among these forms by asking who owns the unhappy path. let-else is good when the current function cannot continue without the shape. if let is good when the work itself is optional. match is better when several alternatives carry domain meaning. A plain let should stay for a shape the type proves. This small rule has helped me avoid both panic-based repairs and deeply nested control flow. The compiler diagnostic is therefore useful design feedback: it tells me that a possibility represented by the type has not yet received an explicit place in the program.
- Which values does the pattern reject?
- What should happen on rejection: return, skip, branch, or propagate?
- Does the continuing path require the binding?
- Is ownership moved, borrowed, or taken from the container?
- Would validation produce a narrower domain type?
- Am I about to replace a compile error with an unjustified unwrap?
- Is error context preserved?
The core principle is that a binding cannot exist conditionally on a path that continues unconditionally. Plain let destructuring must always succeed. When the pattern can fail, I select a control-flow construct that makes the missing case and its policy explicit.