SOP: Error Handling ​
Updated Mar 2026Overview ​
Rust has no exceptions. Errors are values. This SOP covers Result<T, E>, Option<T>, the ? operator, custom error types, and the thiserror/anyhow ecosystem.
Decision Tree ​
Result and Option ​
rust
use std::fs;
use std::io;
// Result<T, E> for operations that can fail
fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
// Option<T> for values that might not exist
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some("Alice".to_string())
} else {
None
}
}The ? Operator ​
rust
use std::fs;
use std::io;
fn read_and_parse(path: &str) -> Result<u32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // io::Error
let number: u32 = content.trim().parse()?; // ParseIntError
Ok(number)
}
// ? on Option — returns None from function
fn first_even(numbers: &[i32]) -> Option<i32> {
let first = numbers.first()?; // Returns None if empty
if first % 2 == 0 {
Some(*first)
} else {
None
}
}Custom Error Types with thiserror ​
rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not found: {entity} with id {id}")]
NotFound { entity: String, id: u64 },
#[error("Validation failed: {0}")]
Validation(String),
#[error("Unauthorized")]
Unauthorized,
}
// Usage
fn get_user(id: u64) -> Result<User, AppError> {
let user = db.find(id).map_err(AppError::Database)?;
match user {
Some(u) => Ok(u),
None => Err(AppError::NotFound {
entity: "User".into(),
id,
}),
}
}anyhow for Applications ​
rust
use anyhow::{Context, Result, bail, ensure};
fn process_file(path: &str) -> Result<()> {
let content = std::fs::read_to_string(path)
.context("Failed to read config file")?; // Adds context
ensure!(!content.is_empty(), "Config file is empty"); // Like assert but returns Err
let config: Config = toml::from_str(&content)
.context("Failed to parse TOML config")?;
if config.version < 2 {
bail!("Config version {} is unsupported", config.version); // Return error
}
Ok(())
}
// In main
fn main() -> Result<()> {
process_file("config.toml")?;
println!("Config loaded successfully");
Ok(())
}Error Handling Patterns ​
Mapping Errors ​
rust
fn parse_port(s: &str) -> Result<u16, String> {
s.parse::<u16>()
.map_err(|e| format!("Invalid port '{}': {}", s, e))
}Combining Option and Result ​
rust
fn get_env_port() -> Result<u16, Box<dyn std::error::Error>> {
let port_str = std::env::var("PORT")?; // Result -> ?
let port: u16 = port_str.parse()?; // Result -> ?
Ok(port)
}
// Option to Result conversion
fn find_config_value(key: &str) -> Result<String, AppError> {
config.get(key)
.cloned()
.ok_or_else(|| AppError::NotFound {
entity: "config".into(),
id: 0,
})
}Collecting Results ​
rust
fn parse_all(strings: Vec<&str>) -> Result<Vec<i32>, std::num::ParseIntError> {
strings.iter()
.map(|s| s.parse::<i32>())
.collect() // Stops at first error, returns Result<Vec<i32>, _>
}When to Use What ​
| Scenario | Use |
|---|---|
| Function can fail, caller handles it | Result<T, E> |
| Value might not exist | Option<T> |
| Bug in logic, should never happen | panic!() / unreachable!() |
| Library with specific error variants | thiserror custom enum |
| Application, error detail matters | anyhow::Result with .context() |
| Quick prototype / test | .unwrap() / .expect("msg") |
| Multiple error types, no custom enum | Box<dyn std::error::Error> |
Anti-Patterns ​
| Do NOT | Do Instead |
|---|---|
.unwrap() in production paths | Use ? or pattern match |
Ignore Result with let _ = | Handle or explicitly document why |
panic!() for expected failures | Use Result and propagate |
Catch-all Box<dyn Error> in libraries | Define specific error types |
| Nested match on Result/Option | Chain .map(), .and_then(), ? |