Skip to content

← Errors Are Values step 23 of 24

Medium Primitives

unwrap and expect discipline

Take a function that works on good input and kills the process on anything else, and give its failures a return type.

pub fn process(rows: Vec<String>) -> Result<Vec<i32>, String>

Each row is key=value. Both halves are trimmed. Three ways a row can be bad, and each one now has a message:

row error
no = at all row `oops` has no `=`
empty (or whitespace-only) key row `=5` has an empty key
non-numeric value row `a=x` has a non-numeric value

The message quotes the whole row, verbatim and untrimmed. On the first bad row, return; the rest are not processed.

The gate is five lints, all opt-in

#![deny(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::unwrap_in_result,
    clippy::panic_in_result_fn
)]

Every one of these is allow by default. Without that attribute this problem would have no gate at all — the starter compiles, and clippy says nothing. That is the honest situation with unwrap in Rust: nothing stops you, and nothing will, unless a project turns these on deliberately.

With them on, the starter produces five errors:

error: used `unwrap()` on an `Option` value
error: used `expect()` on a `Result` value
error: `panic` should not be present in production code
error: `expect` used in a function that returns a `Result`
error: used `panic!()` or assertion in a function that returns `Result`

The last two are the interesting ones. unwrap_in_result and panic_in_result_fn fire only inside a function that already returns Result — which is a sharper rule than a blanket ban. If your signature already says “this can fail”, panicking instead of returning Err is not a judgement call, it is an inconsistency.

(A wart worth knowing: with unwrap_used denied, clippy suggests you use expect instead — even when expect_used is also denied. That is rust-clippy#9222, not you misreading it.)

The real lesson is the message convention

A blanket ban on unwrap does not improve code; it just moves the panic to .expect("failed"). What actually improves code is the convention std itself uses:

The message states the invariant that was violated, phrased as what the code expected — not what went wrong.

// good: names the assumption, so when it fires you know what was wrong about it
.expect("hardcoded IP address should be valid")
.expect("cache should have been primed by init()")
.expect("config schema guarantees this key exists")

// bad: tells you nothing you could not see from the stack trace
.expect("failed to parse")
.expect("should work")
.unwrap()

An expect message is a comment the compiler cannot check but reality can. It fires exactly when it is wrong, in front of the person who has to fix it. That is worth more than the panic itself.

What the payloads actually look like

Verified on this toolchain:

call panic message
Option::unwrap() called `Option::unwrap()` on a `None` value
Option::expect(m) just m
Result::unwrap() called `Result::unwrap()` on an `Err` value: <Debug of E>
Result::expect(m) m: <Debug of E>

So on Result, your message and the error’s Debug are both printed — which is one more reason to derive Debug thoughtfully on error types, and why r.ok().expect(m) is worse than r.expect(m): the .ok() throws away the part that says what actually failed.

Two more lints in the family

  • panicking_unwrap is deny by default — you get it for free. It catches if o.is_some() { o.unwrap() } shapes where the compiler can see the unwrap is redundant, and its sibling unnecessary_unwrap suggests if let instead.
  • get_unwrap fires on v.get(0).unwrap(), which is a longer way of writing v[0].

And a limit worth stating plainly: clippy does not catch every panic. Slice indexing v[0] can panic and does not trip missing_panics_doc. Integer division by zero panics. Arithmetic overflow panics in debug builds. Denying unwrap_used buys you a lot, and it does not buy you a panic-free program.

Notes

  • split_once('=') splits at the first =, so "a=1=2" has value "1=2" — which is a non-numeric value, not a second key.
  • ok_or_else and map_err are the two tools that turn the three panics into three Errs. Both are lazy, so the messages cost nothing on the happy path.
  • Under -D warnings the whole thing is a build failure rather than a warning, which is what these lints are for: they make a code-review comment into a compiler error.

Remember the grade is compile + tests + clippy -D warnings.