Error Handling
TL;DR Rust has no exceptions. Recoverable errors use Result<T, E>; unrecoverable errors use panic!. The ? operator propagates errors up the call stack without boilerplate. Libraries define typed errors with thiserror; applications use anyhow for flexible error context.
Result and ?
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?; // ? = early return on Err
Ok(content)
}
? desugars to: if Err, convert the error with From::from and return early.
Defining Library Errors with thiserror
For library crates, define a concrete error type. thiserror generates Display and Error impl from derive macros.
# Cargo.toml
[dependencies]
thiserror = "2"
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {resource} with id {id}")]
NotFound { resource: &'static str, id: u64 },
#[error("invalid input: {0}")]
Validation(String),
}
#[from] implements From<sqlx::Error> for AppError, so ? converts automatically.
Application Error Context with anyhow
For binary crates and application code, anyhow::Error is a type-erased error that supports adding context messages.
[dependencies]
anyhow = "1"
use anyhow::{Context, Result};
fn load_user(id: u64) -> Result<User> {
let row = db.query_one(id)
.context("failed to query user table")?;
let user = parse_user(row)
.with_context(|| format!("failed to parse user {id}"))?;
Ok(user)
}
anyhow::Result<T> is shorthand for Result<T, anyhow::Error>.
Rule of thumb:
- Library crate →
thiserrorwith typed errors (callers can match on variants) - Application binary →
anyhowfor ergonomic context propagation - Don't mix: an
anyhow::Errorerases type information, so libraries shouldn't return it
Error Conversion
From enables automatic conversion via ?:
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
fn load() -> Result<(), AppError> {
let _ = fs::read("file")?; // io::Error → AppError::Io automatically
Ok(())
}
Panics
Use panic! only for logic bugs — states that should never happen. Never for expected error conditions.
// ok: programmer error (index invariant broken)
fn get(v: &[i32], i: usize) -> i32 {
v[i] // panics on out-of-bounds
}
// bad: network error is expected, not a bug
fn fetch() -> String {
reqwest::get(url).unwrap() // don't do this in production
}
In tests and prototypes, .unwrap() / .expect("msg") are fine. In production library code, always return Result.
Collecting Errors
Collect a Vec<Result<T, E>> into a Result<Vec<T>, E>:
let results: Result<Vec<_>, _> = inputs
.iter()
.map(|x| parse(x))
.collect();
Fails fast: stops at the first error. For all-errors collection, use a Vec<Result<...>> and filter manually.