Deep Dive: Rust Error Handling
Core Philosophy: Type-System Enforced Handling
Rust uses the Result<T, E> enum (Sum Type) to represent success or failure. This forces the caller to handle the error (or explicitly ignore it) at compile time.
Developer Experience (DX)
Ergonomics with ?
Rust combines explicit values with ergonomic propagation. The ? operator is syntax sugar for "unwrap or return early".
fn read_config() -> Result<Config, io::Error> {
let mut file = File::open("config.toml")?; // Returns Err(io::Error) immediately if failed
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(parse(contents))
}
DX Pros:
- Concise:
?removes theif err != nilboilerplate while keeping the "early return" semantics visible. - Chaining: Works well with functional combinators:
File::open(path).and_then(|f| ...)
DX Cons:
- Type Alignment:
?only works if the error type matches the function's return error type (or can be converted viaFrom). This leads to "error soup" where you need a common error enum or a boxed trait object.
The Ecosystem: thiserror vs anyhow
Rust's error handling splits into two main use cases:
-
Libraries (
thiserror): Libraries must export precise error types so callers can handle specific cases.thiserrorderives theErrortrait for enums. -
Applications (
anyhow): Apps usually just want to report errors.anyhow::Result<T>is a wrapper aroundBox<dyn Error + Send + Sync>with easy context attachment.
Implementation Tradeoffs
1. Zero-Cost Abstractions vs. Binary Size
Tradeoff: Result is a value.
- Benefit: Zero runtime overhead for the "happy path" (unlike try-catch setup costs in some languages). The compiler optimizes
Resultlayout (e.g.,Option<Box<T>>is a single pointer, null=None). - Cost: Monomorphization. Generic functions using
Resultgenerate code for everyTandE, potentially increasing binary size.
2. Recoverable vs. Unrecoverable
Rust strictly separates:
Result: Recoverable errors (file not found, network timeout).panic!: Unrecoverable bugs (index out of bounds). Unwinds the stack (or aborts).- Tradeoff: Forces developers to categorize errors. You can't just "catch everything" including panics easily (requires
catch_unwind, which is discouraged for logic control).
3. Exhaustiveness
Tradeoff: match must cover all cases.
- Benefit: Refactoring safety. Adding a new error variant breaks the build, forcing you to handle it.
- Cost: Boilerplate when you just want to pass it up. (Solved mostly by
?).
Summary
Rust provides the safety of checked exceptions with the ergonomics of unchecked exceptions (via ?) and the performance of C return codes. The learning curve lies in managing error types and conversions.