Error Handling Workflow
Handle errors idiomatically in Rust using Result, Option, thiserror, and anyhow.
When to Use
- “error handling”
- “Result type”
- “thiserror”
- “anyhow”
Core Types
Result<T, E>
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
// Usage
match read_file("config.txt") {
Ok(contents) => println!("{}", contents),
Err(e) => eprintln!("Error: {}", e),
}
// With ? operator
fn process() -> Result<(), std::io::Error> {
let contents = read_file("config.txt")?;
println!("{}", contents);
Ok(())
}
Option
fn find_user(id: u64) -> Option<User> {
DATABASE.get(&id).cloned()
}
// Usage
match find_user(42) {
Some(user) => println!("Found: {}", user.name),
None => println!("Not found"),
}
// With ? operator (in function returning Option)
fn get_user_email(id: u64) -> Option<String> {
let user = find_user(id)?;
Some(user.email)
}
Library Errors: thiserror
Use thiserror to define custom error types for libraries.
[dependencies]
thiserror = "2.0"
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error on line {line}: {msg}")]
Parse { line: usize, msg: String },
#[error("User {0} not found")]
NotFound(String),
#[error("Invalid configuration")]
InvalidConfig,
}
// Usage
fn read_config(path: &str) -> Result<Config, MyError> {
let contents = std::fs::read_to_string(path)?; // Auto-converts io::Error
parse_config(&contents)
.map_err(|e| MyError::Parse {
line: e.line,
msg: e.to_string(),
})
}
Automatic Conversions
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)] // Use inner error's Display
Io(#[from] std::io::Error),
#[error(transparent)]
Parse(#[from] serde_json::Error),
}
// Both io::Error and serde_json::Error auto-convert
fn load_json(path: &str) -> Result<Data, Error> {
let contents = std::fs::read_to_string(path)?; // io::Error -> Error
let data = serde_json::from_str(&contents)?; // serde_json::Error -> Error
Ok(data)
}
Application Errors: anyhow
Use anyhow for applications where you want flexible error handling.
[dependencies]
anyhow = "2.0"
use anyhow::{Context, Result};
fn process_file(path: &str) -> Result<()> {
let contents = std::fs::read_to_string(path)
.context("Failed to read configuration file")?;
let config: Config = serde_json::from_str(&contents)
.context("Failed to parse JSON configuration")?;
apply_config(config)
.context("Failed to apply configuration")?;
Ok(())
}
// Custom errors with context
fn validate_user(user: &User) -> Result<()> {
anyhow::ensure!(
user.age >= 18,
"User must be 18 or older, got {}",
user.age
);
if user.email.is_empty() {
anyhow::bail!("Email cannot be empty");
}
Ok(())
}
When to Use What
thiserror - For Libraries
// Define typed errors for library boundaries
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Connection failed: {0}")]
Connection(String),
#[error("Query error: {0}")]
Query(#[from] sqlx::Error),
}
pub fn connect(url: &str) -> Result<Connection, DatabaseError> {
// Library code with typed errors
}
anyhow - For Applications
// Application layer with flexible error handling
use anyhow::Result;
fn main() -> Result<()> {
let config = load_config("config.toml")
.context("Failed to load configuration")?;
let db = connect(&config.database_url)
.context("Database connection failed")?;
run_server(db, config)?;
Ok(())
}
Error Patterns
Wrapping Errors
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error")]
Database(#[from] DatabaseError),
#[error("Network error")]
Network(#[from] NetworkError),
}
Adding Context
// With anyhow
file.read()
.context("Reading user data")
.with_context(|| format!("File: {}", path))?;
// With Result
file.read()
.map_err(|e| format!("Failed to read {}: {}", path, e))?;
Recovering from Errors
// Using or_else
let value = risky_operation().unwrap_or_else(|_| default_value());
// Using or
let value = optional_value.or(Some(default));
// Using ok_or
let result: Result<T, E> = option.ok_or(error)?;
Best Practices
- Libraries: use thiserror - Typed errors at API boundaries
- Applications: use anyhow - Flexible error aggregation
- Never panic in libraries - Return
Resultinstead - Add context - Use
.context()for meaningful errors - Use ? operator - Propagate errors ergonomically
- Document errors - List possible errors in docs
- Don’t use
unwrap()- Except in tests or when impossible to fail
Don’t Do This
// Bad: panic in library
pub fn get_user(id: u64) -> User {
DATABASE.get(&id).unwrap() // DON'T!
}
// Bad: losing error information
fn bad() -> Result<(), String> {
file.read().map_err(|_| "failed".to_string())?; // Lost error details
Ok(())
}
Do This
// Good: return Result
pub fn get_user(id: u64) -> Result<User, Error> {
DATABASE.get(&id)
.ok_or(Error::NotFound(id))
}
// Good: preserve error information
fn good() -> Result<()> {
file.read()
.context("Failed to read user data")?;
Ok(())
}