Rust Error Handling Cheat Sheet
Covers the Result and Option types, the ? operator, custom error types, and idiomatic patterns for propagating and handling errors in Rust.
Result & Option Basics
The two core types for representing fallible or absent values.
fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("division by zero")) } else { Ok(a / b) }}match divide(10.0, 2.0) { Ok(v) => println!("Result: {}", v), Err(e) => println!("Error: {}", e),}// Option<T> for values that may be absentfn find_first_even(nums: &[i32]) -> Option<i32> { nums.iter().find(|&&n| n % 2 == 0).copied()}if let Some(n) = find_first_even(&[1, 3, 4, 5]) { println!("First even: {}", n);}
The ? Operator
Propagating errors concisely up the call stack.
use std::fs::File;use std::io::{self, Read};fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("username.txt")?; // returns early on Err let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s)}// ? also works with Option in functions returning Option<T>fn first_char_upper(s: &str) -> Option<char> { let c = s.chars().next()?; Some(c.to_ascii_uppercase())}
Custom Error Types
Defining a domain-specific error enum with Display and Error impls.
use std::fmt;#[derive(Debug)]enum AppError { NotFound(String), InvalidInput { field: String },}impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::NotFound(what) => write!(f, "not found: {}", what), AppError::InvalidInput { field } => write!(f, "invalid input: {}", field), } }}impl std::error::Error for AppError {}fn lookup(id: u32) -> Result<String, AppError> { if id == 0 { return Err(AppError::NotFound(format!("id {}", id))); } Ok("record".to_string())}
Result/Option Combinators
Common chaining methods for handling values without full match blocks.
- .unwrap()- Returns the Ok/Some value or panics; use only when failure is truly impossible
- .expect("msg")- Like unwrap but with a custom panic message for better diagnostics
- .unwrap_or(default)- Returns the value, or a fallback default if Err/None
- .unwrap_or_else(f)- Returns the value, or computes a fallback from a closure
- .map(f)- Transforms the Ok/Some value, leaving Err/None untouched
- .and_then(f)- Chains a fallible operation that itself returns Result/Option
- .ok()- Converts a Result<T, E> into Option<T>, discarding the error
- .map_err(f)- Transforms the error type/value, leaving Ok untouched
panic! vs Result
When to use unrecoverable panics versus recoverable errors.
- panic!- Unrecoverable error; unwinds (or aborts) the thread — use for bugs/invariants, not expected failures
- Result<T, E>- Recoverable error; forces the caller to explicitly handle or propagate failure
- main() -> Result<(), E>- main can return Result; a returned Err prints Debug output and exits with code 1
- unwrap() in prototypes- Acceptable in examples/tests; replace with proper handling before production
From/Into for Automatic Error Conversion
The ? operator calls From::from on the error, so implementing From lets one function propagate multiple error types.
use std::fmt;#[derive(Debug)]enum AppError { Io(std::io::Error), Parse(std::num::ParseIntError),}impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { AppError::Io(e) => write!(f, "io error: {}", e), AppError::Parse(e) => write!(f, "parse error: {}", e), } }}impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> Self { AppError::Io(e) }}impl From<std::num::ParseIntError> for AppError { fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }}fn read_number(path: &str) -> Result<i32, AppError> { let text = std::fs::read_to_string(path)?; // io::Error -> AppError via From let n: i32 = text.trim().parse()?; // ParseIntError -> AppError via From Ok(n)}
Box<dyn Error> for Heterogeneous Errors
A trait object lets a function return any error type without defining a custom enum, at the cost of losing static type info.
use std::error::Error;fn parse_and_double(s: &str) -> Result<i32, Box<dyn Error>> { let n: i32 = s.parse()?; // ParseIntError auto-coerces into Box<dyn Error> Ok(n * 2)}fn main() -> Result<(), Box<dyn Error>> { let doubled = parse_and_double("21")?; println!("{}", doubled); // source() walks the underlying error chain for diagnostics if let Err(e) = parse_and_double("nope") { eprintln!("error: {e}"); let mut source = e.source(); while let Some(s) = source { eprintln!(" caused by: {s}"); source = s.source(); } } Ok(())}
thiserror Derive Macro
thiserror generates Display and Error impls from attributes, eliminating the boilerplate of hand-written enums.
use thiserror::Error;#[derive(Error, Debug)]enum DataError { #[error("record {id} not found")] NotFound { id: u32 }, #[error("invalid field `{field}`: {reason}")] Invalid { field: String, reason: String }, #[error("database error")] Db(#[from] std::io::Error), // #[from] auto-derives From<io::Error>}fn load(id: u32) -> Result<String, DataError> { if id == 0 { return Err(DataError::NotFound { id }); } Ok("row".into())}
anyhow Context Chaining
anyhow::Context attaches human-readable breadcrumbs to an error as it propagates through call layers.
use anyhow::{Context, Result};fn load_config(path: &str) -> Result<String> { std::fs::read_to_string(path) .with_context(|| format!("failed to read config at {path}"))}fn init() -> Result<()> { let cfg = load_config("app.toml") .context("application startup failed")?; println!("loaded: {cfg}"); Ok(())}// Printing the error with {:#} shows the full chain:// application startup failed: failed to read config at app.toml: No such file...
Advanced Error-Handling Idioms
Patterns beyond basic match/? for production-grade error handling.
- Library vs application errors- Libraries expose typed enums (thiserror) so callers can match variants; applications aggregate with anyhow/eyre since callers only need to report, not branch
- #[non_exhaustive] on error enums- Prevents downstream crates from exhaustively matching, so adding a new variant isn't a breaking change
- std::process::exit(code)- Use a non-zero exit code on unrecoverable startup failure instead of panicking, so the OS/CI sees a clean failure signal
- std::panic::catch_unwind- Catches a panic at an FFI or thread boundary and converts it to a Result; never use it as ordinary control flow
- RUST_BACKTRACE=1- Env var that enables backtrace capture for panics and for anyhow errors created with anyhow!/bail!
- .ok_or(err) / .ok_or_else(f)- Converts Option<T> into Result<T, E>, supplying the error to use when the value is None
- matches! macro- matches!(result, Err(AppError::NotFound(_))) tests a variant without a full match block
Use the `thiserror` crate for library error enums (derives Display/Error with minimal boilerplate) and `anyhow::Result` for application code where you just need to propagate errors with context, not match on variants.