Rust Error Handling with Result
debt(d3/e3/b5/t7)
Closest to 'default linter catches the common case' (d3), clippy flags the code_pattern \.unwrap\(\)|\.expect\( and the compiler's #[must_use] warning catches ignored Results, so the common misuse is detected statically rather than silently in production.
Closest to 'simple parameterised fix' (e3), the quick_fix is to return Result<T, E> and propagate with ?, replacing unwrap/match patterns — a small refactor of a function signature and its call chain rather than a one-line swap.
Closest to 'persistent productivity tax' (b5), since applies_to spans web, cli, queue-worker, and library contexts and error handling is load-bearing; choosing a concrete error type instead of a unifying enum or Box<dyn Error> slows many work streams and forces propagation rework across the codebase.
Closest to 'serious trap' (t7), the misconception is that Rust uses try/catch exceptions and unwrap is the normal way to extract values, contradicting how error handling works in other languages — unwrap silently converts a recoverable error into a process-crashing panic.
Also Known As
TL;DR
Explanation
Rust has no exceptions for recoverable errors. Instead, fallible functions return `Result<T, E>`, an enum with two variants: `Ok(T)` carrying a success value and `Err(E)` carrying an error. Because `Result` is `#[must_use]`, the compiler warns if you ignore it, forcing you to acknowledge that an operation can fail. This makes failure paths visible in type signatures rather than hidden in stack-unwinding control flow.
To extract a value you must handle both variants, typically with `match`, combinators like `map`, `and_then`, `unwrap_or`, or the `?` operator. The `?` operator is the idiomatic propagation tool: in a function returning `Result`, `let f = File::open(path)?;` returns early with the `Err` if the call failed, otherwise unwraps the `Ok` value. `?` also applies the `From` trait to convert the error into the function's declared error type, which is why error-handling crates and custom error enums implement `From` for their sources.
A crucial distinction is `Result` versus `panic!`. `Result` is for expected, recoverable failures - a missing file, invalid input, a network timeout. `panic!` (and the `unwrap`/`expect` methods that trigger it) is for unrecoverable bugs and broken invariants. Reaching for `.unwrap()` everywhere turns recoverable errors into crashes and is a common beginner mistake. `.expect("reason")` is marginally better because it documents the assumption, but production code on real input should propagate or handle, not unwrap.
For error types, libraries commonly use `thiserror` to derive rich enum errors with `Display` and `From` impls, while applications use `anyhow` for a boxed, context-carrying `Result` that is easy to propagate. The `Option<T>` type plays a parallel role for absence (`Some`/`None`) and converts to `Result` via `ok_or`. Combined with `?`, these tools let you write linear, readable code where the happy path dominates and errors flow upward explicitly, giving exhaustive, compiler-checked handling without the invisible control flow of exceptions.
Common Misconception
Why It Matters
Common Mistakes
- Calling .unwrap() or .expect() on Results from fallible I/O or parsing, turning recoverable errors into runtime panics.
- Ignoring a returned Result and triggering only an unused-must-use warning instead of handling the failure.
- Writing nested match blocks to propagate errors when the ? operator would express the same thing in one line.
- Using panic! or .unwrap() for expected conditions like missing config rather than returning an Err.
- Returning a concrete error type from one source so ? cannot convert other error types, instead of a unifying enum or Box<dyn Error>.
Avoid When
- Quick prototypes, examples, or tests where a panic on bad input is acceptable and explicit error types add noise.
- Cases where an Err genuinely represents an unrecoverable broken invariant, where panic! communicates the bug more honestly.
- Code where the value is statically guaranteed present, such as a constant literal parse, and expect documents the impossibility.
When To Use
- Any function performing I/O, parsing, or network calls where failure is an expected runtime condition.
- Library APIs that should let callers decide how to handle failure rather than crashing their process.
- Propagating errors up a call stack concisely with ? while converting between error types via From.
- Building services that must stay alive and respond gracefully when individual requests or inputs are invalid.
Code Examples
use std::fs::File;
use std::io::Read;
// Returns the parsed number, but panics on every failure path.
fn read_count(path: &str) -> u32 {
// unwrap panics if the file is missing.
let mut file = File::open(path).unwrap();
let mut contents = String::new();
// unwrap panics on a read error.
file.read_to_string(&mut contents).unwrap();
// unwrap panics if the contents are not a valid number.
contents.trim().parse::<u32>().unwrap()
}
fn main() {
// A missing file or garbage input crashes the whole program.
let count = read_count("count.txt");
println!("{}", count);
}
use std::fs::File;
use std::io::{self, Read};
use std::num::ParseIntError;
#[derive(Debug)]
enum CountError {
Io(io::Error),
Parse(ParseIntError),
}
// From impls let the ? operator convert each source error automatically.
impl From<io::Error> for CountError {
fn from(e: io::Error) -> Self { CountError::Io(e) }
}
impl From<ParseIntError> for CountError {
fn from(e: ParseIntError) -> Self { CountError::Parse(e) }
}
// Failure is visible in the signature; ? propagates errors cleanly.
fn read_count(path: &str) -> Result<u32, CountError> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let count = contents.trim().parse::<u32>()?;
Ok(count)
}
fn main() {
match read_count("count.txt") {
Ok(count) => println!("{}", count),
Err(e) => eprintln!("could not read count: {:?}", e),
}
}
References
https://doc.rust-lang.org/book/ch09-00-error-handling.html
https://doc.rust-lang.org/std/result/enum.Result.html
https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator
https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved-c-good-err