RFA-077 · Case file with fixtures · Case 49 of 694 · Compiler evidence
E0080: Why a Panicking Constant Expression Fails the Rust Build
Required const evaluation must produce a valid value before the program exists. Represent invalid input with Option or Result when it is data, and reserve compile-time panic for an intentional invariant assertion.
- Reviewed
- Rust
- Rust 1.98.1
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Required const evaluation executes during compilation, and an invalid operation such as division by zero makes the constant impossible to produce.
- First discriminating check
- Move the operation into a const function returning `Option` or `Result` and inspect the invalid case without forcing a panicking value.
A constant initializer is executed by the compiler when its value is required. An invalid operation cannot be postponed:
#![allow(unconditional_panic)]
const BROKEN: u32 = 10 / 0;
Rust 1.98.1 reports E0080: evaluation of BROKEN failed because it attempted to divide by zero. The failing fixture allows the separate unconditional-panic lint so the const-evaluation failure is isolated.
No execution profile can make BROKEN valid. The compiler must place a u32 value into the program, and evaluation produced none.
That failed value cannot remain deferred inside the binary.
Const evaluation is execution with stricter boundaries
The Rust Reference const-evaluation chapter lists expressions allowed in constant contexts. When evaluation is required, overflow, out-of-bounds indexing, division by zero, and explicit panic can become compile-time errors.
This is different from a lint warning about suspicious code. E0080 says the compiler evaluated a required value and reached an invalid state. Adding allow(unconditional_panic) can silence a lint, but it cannot manufacture the missing constant.
Represent an invalid input as data
If zero is a possible configuration input, a checked operation expresses it:
const fn checked_ratio(left: u32, right: u32) -> Option<u32> {
left.checked_div(right)
}
const RATIO: Option<u32> = checked_ratio(10, 0);
The constant now evaluates successfully to None. The repaired fixture compiles, runs, and checks that result on Rust 1.98.1.
This is correct when invalidity is expected data. Consumers must handle None, and the type retains the reason that no numeric result exists.
Compile-time panic can be intentional
If zero indicates a programmer error in a fixed table or generated configuration, failing the build can be desirable:
const fn ratio(left: u32, right: u32) -> u32 {
assert!(right != 0, "ratio denominator must be nonzero");
left / right
}
A const call with zero still produces E0080, now with a domain-specific assertion message. This turns the failure into a checked invariant rather than an accidental arithmetic panic.
I choose between Option and panic from who controls the input. Recoverable external data should not normally become a compile failure. Source-level invariants can.
Generic constants can fail only for some instantiations
A const expression depending on generic parameters may be evaluated when a particular type or value is instantiated. The defining code can look harmless, while one downstream use triggers E0080 with a long substitution trace.
I reduce the failing instantiation to concrete values and find the first invalid operation in the diagnostic chain. Increasing recursion limits or changing optimization does not repair a bad evaluated value.
Array lengths make this especially visible. A calculation that underflows can fail while forming [T; N], before any function using the array is called. Const generic callers can therefore expose an invalid boundary value in a library expression that compiled for other values.
I keep a small compile-fail test for intentionally rejected instantiations and a normal test for accepted boundary values. This distinguishes a protected invariant from an accidental evaluator failure after later refactoring.
Runtime and compile-time overflow policies should not be confused
Ordinary runtime integer overflow can depend on profile settings for operators. Required constant evaluation uses its own rules and cannot embed an already-invalid result merely because one release profile might wrap at runtime.
If wrapping is the intended arithmetic, I use explicit methods such as wrapping_add. If checked failure is intended, I use checked_*. The operation name then preserves semantics in both constant and runtime contexts.
A longer evaluation trace is evidence
For array lengths, discriminants, const generics, and static initializers, E0080 can include several “inside” frames. I read them like a runtime stack trace:
- Start at the constant the program requires.
- Follow each evaluated const function call.
- Locate the first panic or invalid primitive operation.
- Record the concrete generic values at that point.
- Decide whether invalid input should be represented or rejected.
The official E0080 explanation shows a divide-by-zero constant. The important design question comes after reproducing it: is the failed operation proving a source invariant, or is it encountering a legitimate absence? Use a clear compile-time assertion for the first and a checked result type for the second.