expect Is for Invariants Not Input

expect is for invariants, not input

The panics dimension of the Idiomatic Rust Rubric allows one exception to "no unwrap/expect in production": a documented infallible invariant, written as expect("why this can't fail"). The sharp line people miss is what can carry that exception. expect is legitimate for an invariant the program itself guarantees. It is never legitimate for data that came from outside the program.

The two kinds of "this can't fail"

The bug that names the rule

Mxr's snooze resolver did this:

let time = NaiveTime::from_hms_opt(hour, 0, 0).expect("validated snooze hour");

hour came from morning_hour: u8 in the user's TOML config. Nothing validated it — the "validated" in the message was a lie the author told themselves. A hand-edited morning_hour = 99 makes from_hms_opt return None, the expect panics, and because this runs in the daemon, the panic takes down background sync for every client. A one-character config typo, a process-wide outage.

The fix bounds the input before it reaches the panic, so the invariant becomes real:

// clamp at the boundary: a config value is input, not a guarantee
let time = NaiveTime::from_hms_opt(hour.min(23), 0, 0).expect("hour clamped to 0..=23");

Now the expect describes something the code actually enforces one line up. Returning a Result and surfacing a config error works too; the point is that the impossible value is handled, not asserted away.

The grep that finds them

Read your expect/unwrap messages. Any that say "validated", "should be valid", "checked", or "always" about data with an external origin is a latent panic — the message is doing the validation that the code skipped. The legitimate ones name a program-internal fact: a string literal, an exhaustive match, a value you constructed three lines above.

Prove the fix

A behaviour test feeds the impossible value and asserts no panic plus the clamped result — and it has teeth: delete the clamp and the test panics instead of asserting. That mutation-kill check is the difference between a real test and a decorative one. See Refactor Behind a Behavior Test.

See also