Error handling
Goal
Recoverable failure is Result. Panics are bugs, not control flow.
Inputs / outputs
- In: a fallible operation
- Out:
Result<T, E>with a usefulE, or a typed panic only for invariants
Reads / writes
- Read: the fallible fn and its callers
- Write: none unless a playbook opened the task
Approval
Adding thiserror / anyhow if they are not already in Cargo.toml.
RULES — no exceptions
- Library crates:
thiserror(or a small handwrittenenum) implementingError+From - Binary / app crates:
anyhow(or the lib error) with.context() ?to propagate;Fromimpls make?work- No
.unwrap()on user/IO/parse paths .expect("…")only for proven invariants (a bug if it fires)- Messages: lowercase, no trailing punctuation
- Preserve
source(#[source]/.context()) - Document
# Errorson public fallible fns
Example
use std::num::NonZeroU16;
// ❌
let n: u16 = s.parse().unwrap();
// ✅ lib
#[derive(Debug, thiserror::Error)]
enum ParsePortError {
#[error("not a number")]
Num(#[from] std::num::ParseIntError),
#[error("port must be non-zero")]
Zero,
}
fn parse_port(s: &str) -> Result<NonZeroU16, ParsePortError> {
let n: u16 = s.parse()?;
NonZeroU16::new(n).ok_or(ParsePortError::Zero)
}
Steps
- Classify: recoverable vs invariant
- Recoverable →
Result+? - If the crate is a lib, keep
Etyped; if a bin,anyhowis enough
Validation
No new unwrap/expect on IO/parse. cargo test covers the error variant.
Pitfalls
| ❌ | ✅ |
|---|---|
unwrap in a lib |
? |
expect("failed") on a missing file |
Err + context |
Swallow with let _ = |
return or log once at the edge |
Integration
Predecessor: rust-essentials. Successor: none.