name: rust-error-handling
description: | Use when this capability is needed.
metadata:
author: adxptived
Quick Navigation
Rust Error Handling
Complete guide to idiomatic error handling in Rust, from basics to production patterns.
Philosophy
Rust distinguishes between:
- Recoverable errors →
Result<T, E> (file not found, parse error)
- Unrecoverable errors →
panic! (index out of bounds, invariant violation)
Key principle: Return errors, don't panic. Let callers decide how to handle.
The Basics
Result<T, E>
enum Result<T, E> {
Ok(T),
Err(E),
}
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
match read_file("config.toml") {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("Error: {}", e),
}
Option
enum Option<T> {
Some(T),
None,
}
fn find_user(id: u32) -> Option<User> {
users.iter().find(|u| u.id == id).cloned()
}
if let Some(user) = find_user(42) {
println!("Found: {}", user.name);
}
The ? Operator
Propagates errors automatically:
fn process() -> Result<Data, Error> {
let content = std::fs::read_to_string("file.txt")?;
let parsed = parse(&content)?;
let validated = validate(parsed)?;
Ok(validated)
}
? with Option
fn first_even(numbers: &[i32]) -> Option<i32> {
let first = numbers.first()?;
Some(*first)
}
? in main()
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = read_config()?;
run_app(config)?;
Ok(())
}
Combinators
Option Methods
option.map(|v| v.to_string())
option.and_then(|v| other_option(v))
option.unwrap_or(default)
option.unwrap_or_default()
option.unwrap_or_else(|| compute())
option.ok_or(error)
option.ok_or_else(|| make_error())
option.filter(|v| v > 0)
option.take()
option.is_some()
option.is_none()
Result Methods
result.map(|v| v.to_string())
result.map_err(|e| wrap_error(e))
result.and_then(|v| other_result(v))
result.unwrap_or(default)
result.unwrap_or_default()
result.unwrap_or_else(|e| handle(e))
result.ok()
result.err()
result.is_ok()
result.is_err()
Chaining Example
fn get_user_email(id: u32) -> Option<String> {
find_user(id)
.and_then(|user| user.contact)
.map(|contact| contact.email)
.map(|email| email.to_lowercase())
}
fn fetch_data(url: &str) -> Result<Data, AppError> {
reqwest::get(url)
.map_err(AppError::Network)?
.json()
.map_err(AppError::Parse)
}
Custom Error Types
Manual Implementation
use std::fmt;
use std::error::Error;
#[derive(Debug)]
pub enum AppError {
NotFound(String),
InvalidInput { field: String, message: String },
Io(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound(id) => write!(f, "Not found: {}", id),
Self::InvalidInput { field, message } => {
write!(f, "Invalid {}: {}", field, message)
}
Self::Io(e) => write!(f, "IO error: {}", e),
}
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(e) => (e),
_ => ,
}
}
}
<std::io::Error> {
(err: std::io::Error) {
AppError::(err)
}
}
thiserror (Libraries)
Derive macro for error types. Best for libraries with typed errors.
[dependencies]
thiserror = "2"
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataError {
#[error("Failed to read file: {path}")]
ReadError {
path: String,
#[source]
source: std::io::Error,
},
#[error("Parse error at line {line}: {message}")]
ParseError { line: usize, message: String },
#[error("Validation failed: {0}")]
ValidationError(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Attributes
| Attribute | Effect |
|---|
#[error("...")] | Generates Display impl |
#[source] | Sets Error::source() |
#[from] | Generates From impl |
#[transparent] | Forwards Display/source |
anyhow (Applications)
Flexible error handling for application code.
[dependencies]
anyhow = "1"
use anyhow::{anyhow, bail, Context, Result};
fn process() -> Result<()> {
let config = read_config()
.context("Failed to read configuration")?;
if config.is_empty() {
bail!("Configuration is empty");
}
let data = fetch_data()
.map_err(|e| anyhow!("Fetch failed: {}", e))?;
Ok(())
}
fn main() {
if let Err(e) = process() {
eprintln!("Error: {:#}", e);
for cause in e.chain() {
eprintln!(" Caused by: {}", cause);
}
std::process::exit(1);
}
}
anyhow Features
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
}
let root = e.root_cause();
let result = operation()
.with_context(|| format!("Failed for user {}", user_id))?;
Library vs Application
Library Code → thiserror
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyLibError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Not found")]
NotFound,
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub fn process(input: &str) -> Result<Output, MyLibError> {
}
Application Code → anyhow
use anyhow::{Context, Result};
use my_lib::{process, MyLibError};
fn main() -> Result<()> {
let result = process("input")
.context("Processing failed")?;
Ok(())
}
Error Patterns
Wrapping External Errors
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Database error")]
Database(#[source] sqlx::Error),
#[error("HTTP error")]
Http(#[source] reqwest::Error),
}
impl From<sqlx::Error> for ServiceError {
fn from(e: sqlx::Error) -> Self {
ServiceError::Database(e)
}
}
Error Context
use anyhow::Context;
fn load_user(id: u32) -> Result<User> {
let path = format!("/data/users/{}.json", id);
let content = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read user file: {}", path))?;
let user: User = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse user {}", id))?;
Ok(user)
}
Multiple Error Types
fn complex_operation() -> Result<Data, Box<dyn Error>> {
let file = std::fs::read_to_string("data.json")?;
let data: Data = serde_json::from_str(&file)?;
Ok(data)
}
fn complex_operation() -> anyhow::Result<Data> {
let file = std::fs::read_to_string("data.json")?;
let data: Data = serde_json::from_str(&file)?;
Ok(data)
}
Recoverable Errors
fn process_batch(items: Vec<Item>) -> (Vec<Output>, Vec<Error>) {
let mut successes = Vec::new();
let mut failures = Vec::new();
for item in items {
match process_item(item) {
Ok(output) => successes.push(output),
Err(e) => failures.push(e),
}
}
(successes, failures)
}
When to Panic
Panic for:
- Programming bugs (violated invariants)
- Impossible states
- Setup failures (can't recover anyway)
- Tests
Don't panic for:
- User input errors
- Network/IO errors
- Missing optional data
- Library code
fn get_item(index: usize) -> &Item {
self.items.get(index)
.expect("index validated in new()")
}
fn parse_config(content: &str) -> Result<Config, ParseError> {
}
unwrap vs expect
let value = option.unwrap();
let value = option.expect("value set during initialization");
let value = option.ok_or(Error::MissingValue)?;
Formatting & Best Practices
1. Lowercase Error Messages
Standard library convention states that error messages returned by std::fmt::Display implementations or in anyhow contexts must be written in lowercase and not end in trailing punctuation (e.g. no periods or exclamation marks).
let err = anyhow!("Failed to process transaction.");
let err = anyhow!("failed to process transaction");
2. Error Source Chaining
When defining custom errors, always annotate the lower-level error causing the failure with #[source] so that the backtrace or debug logs can chain down to the original root cause.
#[derive(thiserror::Error, Debug)]
pub enum DatabaseError {
#[error("connection failed")]
ConnectionFailed(#[source] sqlx::Error),
}
Quick Reference
| Crate | Use For | Error Type |
|---|
| thiserror | Libraries | Typed enum |
| anyhow | Applications | anyhow::Error |
| eyre | Apps (fancy output) | eyre::Report |
| Method | Effect |
|---|
? | Propagate error |
.context("msg") | Add context (anyhow) |
.ok_or(err) | Option → Result |
.map_err(f) | Transform error |
.unwrap_or(v) | Default value |
Production Checklist
- Libraries expose typed errors with
thiserror or manual Error implementations.
- Applications add context at IO, parsing, network, and process boundaries.
- Error messages are lowercase and omit trailing punctuation.
unwrap/expect is limited to tests, examples, and documented invariants.
- Error chains preserve sources for observability and debugging.
- Public APIs document expected failure modes and panic behavior.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.