RFA-600 · Case file with fixtures · Case 572 of 694 · Compiler evidence
Derived Default for a Rust Enum Needs One Explicit Variant
An enum default is a domain policy, not a structural zeroing rule. Mark one unit variant or write a manual implementation when construction carries data.
- 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
- Enum alternatives require a domain policy choice, while derive has no structural rule that can identify which complete value is the useful baseline.
- First discriminating check
- Decide whether a true context-free default exists, then mark one unit variant or implement payload construction manually.
For a struct, Default can be derived by defaulting each field. An enum represents alternatives, so rustc cannot infer which alternative means absence, safety, or normal operation. The failing fixture derives Default for Mode without choosing a variant and gets E0665.
A default is observable policy
Choosing Active could start behaviour or grant capability. Choosing Passive could disable a system that callers expected to run. Declaration order is not a safe policy, and the first discriminant is not automatically the semantic neutral element.
The official E0665 explanation requires one unit variant marked #[default] for derived implementation. If the default variant contains payload, I implement Default manually and construct that payload deliberately.
This is more than a compiler ceremony. unwrap_or_default, struct update syntax, builders, deserializers, and generic containers may invoke the choice far from the enum declaration.
The repair selects Passive explicitly
The repaired fixture marks Passive and asserts that Mode::default() returns it. That is plausible for the example because doing nothing is safer than activating unspecified work.
I still document the meaning. Does passive mean disabled by user choice, not yet configured, temporarily paused, or inherited from a parent? If these states affect behaviour differently, one variant named Passive may collapse important information.
Sometimes there should be no default. Requiring callers to choose a mode prevents accidental implicit policy. I do not implement Default only because a derive list looks complete or a generic API makes it convenient.
Default should produce a useful valid value
The Default trait conventionally constructs a useful baseline. It does not promise zero bytes or the numerically smallest discriminant. Every invariant of the type must hold.
For configuration, default values should be operationally safe and stable enough for callers. Changing them can alter deployments without a compilation error. I treat a public default change as behavioural compatibility work and mention it in release notes.
Security-sensitive types deserve extra care. A default permission should not accidentally grant access. A default cryptographic algorithm or network address may become obsolete. Explicit constructors can force policy to be chosen at a higher layer that has context.
Payload variants need a manual decision
The derive marker is limited to a unit variant. If Mode::Retry { attempts: NonZeroU8 } should be default, its payload needs construction logic. A manual implementation can call a named constructor so validation stays central.
I avoid placeholder payloads that immediately require replacement. A default object should be usable within its documented scope. If no universally valid payload exists, absence can be represented by Option<Mode> instead of inventing one.
There is a semantic difference between None and Some(Mode::default()). The first says no choice exists; the second says a concrete baseline choice exists. Serialization defaults should preserve that distinction where it matters.
Derived defaults interact with evolution
Adding enum variants does not change an explicit default automatically, which is good. Renaming or removing the marked variant requires a conscious migration. Tests at construction boundaries can detect changes in serialized or user-visible behaviour.
I search every use of default() before selecting a variant. If it appears in error recovery, configuration merging, and tests, those contexts may want different policies. Named constructors such as Mode::safe_startup() or Mode::for_tests() can communicate better than one global default.
I also keep examples explicit: a struct containing Mode should show whether it inherits the enum default or chooses another mode. This makes layered configuration behaviour visible to readers.
My E0665 checklist
- Which variant, if any, is safe and useful without caller context?
- Is exactly one unit variant marked
#[default]? - Does a payload require a manual implementation and validated constructor?
- Are absence and a concrete default being confused?
- Can default activate work, grant access, or hide missing configuration?
- Which generic helpers and deserializers call Default indirectly?
- Would named constructors express context-specific policies better?
- Are behavioural and serialization tests protecting the selected policy?
The core principle is that an enum encodes choice, and a default chooses on the caller's behalf. E0665 makes that decision visible. I provide it only when the domain has a genuine baseline and test the behaviour wherever implicit construction can occur.