RFA-450 · Case file with fixtures · Case 422 of 694 · Compiler evidence
Rust or-pattern Alternatives Must Bind the Same Variables
Every branch of an or-pattern must establish the same bindings before one shared arm body runs. Split alternatives whose data differs, or bind equivalent fields with the same names and types in every branch.
- 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
- Every route through an or-pattern enters one body with one statically known binding environment, but Empty cannot initialise Data's payload binding.
- First discriminating check
- List the bindings produced by every alternative and split arms whenever one variant does not carry an equivalent value.
I combined Message::Data(value) | Message::Empty into one match arm because both variants seemed to need similar output. Rust returned E0408: value is not bound in all patterns.
The failing fixture shows why this is more than a naming restriction. One arm body follows both alternatives, but only Data contains a value. If Empty matches, no honest value exists for the body to read.
The body receives one binding environment
An or-pattern means that any alternative may select the same arm. Once selected, Rust does not run a different body for each alternative. It enters one body with one statically known set of local variables.
The E0408 explanation therefore requires each alternative to bind the same variables. Otherwise a name could be uninitialised on one path.
This resembles control-flow joining in an if: values available after the join must exist on every incoming path.
Split arms when the variants carry different information
The repaired fixture gives each variant its own arm:
match message {
Message::Data(value) => value.to_string(),
Message::Empty => String::from("empty"),
}
This is usually the clearest repair. The difference in payload is a real domain difference, so the control flow should display it.
I avoid inventing a default value only to preserve a combined arm. A fake zero, empty string, or sentinel can erase the fact that the payload did not exist.
Combine alternatives that expose equivalent data
Or-patterns remain useful when both alternatives can establish the same binding:
match pair {
(0, value) | (value, 0) => println!("{value}"),
_ => {}
}
Here value exists in both cases and has the same type. Its position differs, but the body receives one consistent local meaning: the non-zero-side candidate.
The patterns Reference calls this static semantic rule out for bindings in every alternative.
Wildcards do not create a value
Changing Message::Empty to _ does not repair the binding. A wildcard matches without binding anything. The body still cannot know what value should mean on that path.
I either split the arm or restructure the data so every alternative genuinely contains the same concept.
Guards run after the pattern
A guard cannot manufacture a missing binding. Rust must first match an alternative and create its binding environment, and only then can it evaluate the guard.
This is invalid in principle even if a guard appears to exclude the branch without the value. The pattern itself must establish all names used by the arm.
When conditions differ, separate arms usually communicate the decision better.
Macros can make the inconsistency hard to see
I have seen this shape appear after a macro expands several variants into one arm. The source invocation may look uniform while one variant lacks a field emitted for the others.
My debugging step is to reduce the expansion to two alternatives and list their bindings on paper. I compare names, types, mutability, and binding modes. E0408 is specifically about a missing name; E0409 covers a name bound in different ways.
Similar output does not require one arm
Sometimes two arms need the same final operation but acquire data differently. I first normalise each case into a common value, then call shared logic after the match:
let text = match message {
Message::Data(value) => value.to_string(),
Message::Empty => String::from("empty"),
};
send(text);
This removes duplicated effects without pretending both variants have identical structure.
My E0408 checklist
I also check whether the combined syntax is saving anything meaningful. Ten alternatives sharing a two-line body can be readable, but an or-pattern with many different nested shapes becomes difficult to diagnose and change. Extracting a small normalisation function or using separate arms may produce more code and less cognitive load. The goal is not the fewest match arms. The goal is one clear explanation for how each admitted state becomes the data required by the next operation.
- Which alternative omits the reported name?
- Does that variant truly contain an equivalent value?
- Would separate arms preserve a meaningful distinction?
- Am I inventing a sentinel only to combine syntax?
- Does a macro expansion generate uneven payload patterns?
- Do all alternatives bind the same names and types?
- Can shared work move after a normalising match?
The core principle is that an or-pattern shares one continuation. Every route into that continuation must create the same usable locals. I combine alternatives only when their bound information is genuinely equivalent; when it is not, separate arms make the type's meaning visible.