Fail Closed, Case Match, Never Fall Back
The Rule
Every branch in conditional logic must be an explicit, named case. There are no fallbacks. There are no degraded modes. If the system cannot prove which case applies, it rejects the request.
Why
A fallback is a silent lie. It tells the caller "everything is fine" when the system actually failed to satisfy the original intent. That hides broken infrastructure, creates invisible state drift, and makes debugging harder because the failure never surfaces.
A case match is an explicit contract. Each branch has a name, a reason, and a test. If a new situation arises that does not match any case, the code fails loudly instead of silently routing to a weaker path.
What This Means In Practice
Do: name every branch
if fitsHotplug && fitsHost {
return Hotplug
}
return LiveMigration
Do not: use else as a catch-all degraded path
// BAD: silent fallback
if canHotplug {
hotplug()
} else {
sleepAndRestart() // "just in case"
}
The second branch is not a case match. It is a bucket for "everything else,"
which means any new failure mode silently routes to sleepAndRestart without
anyone noticing.
Do: treat the safe path as its own named case, not a fallback
ColdMigration (sleep, reconfigure, wake) is not a degraded fallback. It
is the path the system takes when a primary path encounters an error
mid-execution. Everything is pre-allocated before the swap, so the
interruption is bounded and deterministic.
It has its own tests, its own invariants, and its own reason code. It is not "what happens when we give up."
Do: return typed errors instead of routing to a weaker mode
// GOOD: fail closed
match action {
ResizeAction::Hotplug => hotplug(spec),
ResizeAction::LiveMigration => Err(ResizeError::NotImplemented),
ResizeAction::ColdMigration => Err(ResizeError::NotImplemented),
}
// BAD: silent fallback
if let Err(_) = hotplug(spec) {
cold_migrate(spec) // hides the hotplug failure
}
Do: test every case independently
Each named branch should have its own test that proves:
- the branch is reachable under the expected conditions
- the branch produces the expected outcome
- no other branch fires for those conditions
Table-driven tests with one row per case are the natural fit.
Where This Applies
- resize strategy selection (hotplug vs live migration vs cold migration)
- compaction candidacy (warm-capacity guard, packing feasibility, blocker detection)
- workspace lifecycle actions (create, wake, sleep, destroy)
- error classification (retriable vs terminal vs invalid)
- any
switch,match, or multi-branchifthat selects a mode or strategy
Where This Does NOT Apply
- pure data transforms where a default is semantically correct
(e.g.,
max(0, value)is not a fallback; it is a clamp) - configuration normalization where a missing field has a well-defined default
(e.g.,
if gridStep <= 0 { return DefaultGridStep })
The distinction: a default is "the caller did not specify, and the correct answer is known." A fallback is "the system failed, and we are pretending it did not."
Canonical References
style/style.md: "Fail closed. Silent fallback to a weaker mode is usually a lie that hides broken infrastructure."rust-neckbeard/SKILL.md: "In no circumstances should we write fallbacks. Conditional logic should be viewed as case matching, as opposed to fallbacks."resize_strategy.go: reference implementation of exhaustive case matching for a multi-path decision.