RFA-454 · Case file with fixtures · Case 426 of 694 · Compiler evidence
A Tuple Variant Pattern Must Destructure Its Payload
A pattern must use the constructor form declared by the enum: bare path for unit variants, parentheses for tuple variants, and braces for struct variants. Bind or ignore payloads explicitly.
- 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
- A bare path describes unit shape, while the resolved variant is tuple-shaped and requires its positional payload to be bound or explicitly ignored.
- First discriminating check
- Read the enum declaration and mirror unit, tuple, or struct constructor form before deciding how each payload affects behaviour.
I matched State::Failed as a bare path even though Failed was declared as Failed(String). Rust reported E0532: it expected a unit struct, unit variant, or constant but found a tuple variant.
The failing fixture shows a subtle mistake: the variant name resolves correctly, yet the pattern still describes the wrong constructor shape.
Enum variants have distinct product shapes
A unit variant such as State::Ready carries no fields and is matched with a bare path. A tuple variant such as State::Failed(String) carries positional fields and needs parentheses. A struct variant uses named fields and braces.
The E0532 explanation asks the pattern arm kind to match the expression's declared kind. I read the enum definition first because the name alone does not reveal payload shape.
Bind the payload when it explains the state
The repaired fixture uses:
State::Failed(message) => message,
This is often the best repair because the payload exists to carry useful context. In production systems, dropping an error message, retry reason, or status code can make operations harder even when the branch itself is handled.
The binding mode follows the scrutinee. Matching by value may move a String; matching a reference normally produces a borrowed payload through match ergonomics.
Ignore deliberately with underscore
If the payload truly has no relevance, I write State::Failed(_). The underscore accounts for one positional field without introducing a local.
For several tuple fields, I can use one underscore per reviewed position or State::Failed(..) for a deliberate remainder. The explicit syntax tells a reader that I know data is present but this operation does not use it.
A bare State::Failed would falsely describe the variant as data-free, which is why Rust rejects it.
Constructor references are different from values
State::Failed by itself can be used in expression contexts as the constructor function from String to State. In pattern position, however, a bare path has the grammar and meaning of a unit-like value pattern.
That context difference explains why the name may work with iterator mapping such as .map(State::Failed) but fail as one match arm. Adding parentheses in a pattern is destructuring, not calling.
Payload changes are API changes
If a variant changes from unit to tuple form, every matching site must reconsider the new information. E0532 makes that migration visible.
I resist repairing all sites mechanically with _ until I know whether the payload changes policy. A new retry delay or source category might deserve logic in scheduling, telemetry, and user-facing errors.
Compiler pressure is valuable here because the data model evolved for a reason.
Named variants can improve long-lived protocols
Tuple fields remain concise when their meaning is obvious. As a variant grows, named fields can make matching and construction safer:
State::Failed { message, retryable }
This is a design choice, not an E0532 requirement. I consider it when positional payload changes repeatedly or when two fields share the same primitive type.
The Rust Book pattern examples show destructuring across these enum forms.
Macros must preserve constructor kind
Generated matches can lag behind an enum schema. A macro may still emit a bare variant path after the variant acquired fields.
I test generated matches against representative unit, tuple, and struct variants. The error appears at expansion output, but the root cause is usually that the generator's model of variant shape is incomplete.
My E0532 checklist
There is also a useful testing consequence. When I ignore a payload with _, tests should still construct representative payload values so the arm is exercised under realistic ownership and size. If the value contains a guard object or a handle with Drop, ignoring it does not mean lifecycle no longer matters. Pattern shape and runtime destruction are separate. I verify when the complete enum value is dropped and whether moving a bound payload changes that timing.
- Is the item a unit, tuple, or struct variant?
- How many fields does the current declaration carry?
- Does the payload affect domain behaviour or diagnostics?
- Should I bind, borrow, or deliberately ignore it?
- Did the enum shape change during a migration?
- Is generated code preserving the constructor kind?
- Would named fields make future evolution clearer?
The core principle is that a variant name is only part of its structure. Rust patterns must describe whether and how data is carried. I mirror the declaration, then make an explicit decision about every payload instead of silently treating a data-bearing state as an empty label.