Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-440 · Case file with fixtures · Case 412 of 694 · Compiler evidence

A Rust match Must Cover Every Possible Enum Variant

Match exhaustiveness is proven from the scrutinee type, not observed call paths. Handle each meaningful variant for compiler-checked evolution, or add a deliberate catch-all only when unknown cases truly share one policy.

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
Rust proves coverage from every value admitted by the scrutinee type; observed call paths do not remove variants from that set.
First discriminating check
List every enum variant and decide whether each deserves an explicit arm or a deliberate observable fallback.

I matched a two-variant state and handled only the branch seen in my test. Rust reported E0004 because State::Failed remained possible.

The failing fixture calls no function at all. Rust rejects the function body from its type-level possibilities: a State can be Ready or Failed, and the match has no answer for the second value.

Exhaustiveness is independent of today's data

The E0004 explanation says the compiler cannot guarantee a matching pattern for one or more possible inputs. A match expression must always decide control flow and, when used as a value, produce a result.

It does not inspect current database rows, configuration, or the only call in this crate. Those facts can change outside the type system. Rust proves coverage from the scrutinee type and patterns.

This makes an enum a strong state model: every value belongs to one declared variant, and every complete match accounts for that set.

Explicit arms turn new states into migration work

The repaired fixture handles both variants and returns a label for each. If I later add State::Paused, each exhaustive match becomes a compiler-generated review list.

I value this during protocol and workflow changes. Search-based migrations can miss aliases, macros, and downstream logic; exhaustiveness finds typed decision points.

The extra work is useful when each state has different semantics.

A wildcard deliberately gives up that alarm

Adding _ => "unknown" also satisfies exhaustiveness. It can be correct when every unmentioned value has one stable policy. It also means a future variant silently enters that policy.

For telemetry formatting, a generic “other” label may be acceptable. For authorization, billing, persistence, or state-machine transitions, silently treating a new state like old leftovers can be dangerous.

I write the catch-all because the domain has a catch-all, not merely because it shortens code.

Binding a fallback keeps diagnostic context

When the value is still useful, I bind it:

other => log_unhandled(other),

This requires ownership or borrowing compatible with later use. It makes unexpected states observable, but it still does not force a compile error when the enum grows.

For non-Copy values, I often match by reference when the fallback and caller both need access.

Guards do not make coverage automatic

A guarded arm such as State::Ready if feature_enabled() does not cover every Ready. The guard can be false, so another unguarded arm must handle that possibility.

I treat guards as runtime filters layered after structural matching. Exhaustiveness remains about guaranteed pattern coverage.

This is easy to miss when the guard looks logically certain from application configuration. The compiler cannot use arbitrary runtime predicates as proofs.

Non-exhaustive external enums need a fallback

A dependency may mark an enum #[non_exhaustive], requiring downstream matches to include a wildcard because future compatible releases may add variants. Inside the defining crate, explicit coverage can still be available.

This is a versioning contract. My fallback records enough context to diagnose a newer variant and chooses a safe policy rather than assuming it cannot happen.

Boolean and integer matches have the same principle

Enums make the missing case readable, but E0004 also appears for other finite or partitionable values. A boolean match needs both values unless a wildcard covers one. Integer ranges and literals need to cover everything or fall through.

The match-expression Reference defines match arms and selection. I test boundaries whenever ranges divide a domain.

My exhaustiveness review

In reviews, I also ask what the fallback would hide operationally. Returning a generic error may keep compilation green, but it can erase the difference between a retryable state, a rejected request, and a programmer mistake. For internal enums that I control, spelling out the variants usually gives me a better migration alarm. For an external non-exhaustive enum, I keep the wildcard but make the fallback observable with a metric, log field, or preserved value when that information is useful. This is how exhaustiveness becomes more than syntax: it connects the type model to support and incident behaviour.

  • Which values can the scrutinee type represent?
  • Is a guard leaving an otherwise matched value uncovered?
  • Should future enum variants trigger compilation failures?
  • If a wildcard exists, is its domain policy documented?
  • Does an external non-exhaustive enum require forward handling?
  • Should the fallback retain and report the unexpected value?
  • Do all arms return one compatible result type?

The core principle is that a match is a total decision over the values it claims to accept. Rust asks me to prove that totality from types. I use explicit variants when change should demand review and catch-all patterns only when the domain genuinely supplies a safe general answer.