Rust Error Handling
Result<T, E>
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path)
}
match read_config("/etc/app.conf") {
Ok(contents) => println!("{contents}"),
Err(e) => eprintln!("Error: {e}"),
}
Result is an enum: Ok(T) for success, Err(E) for failure. The compiler forces you to handle both.
The ? Operator
fn read_port(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
let contents = fs::read_to_string(path)?; // return Err if it fails
let port: u16 = contents.trim().parse()?; // return Err if it fails
Ok(port)
}
? unwraps Ok or returns Err from the enclosing function. It also converts between error types if From is implemented. Box<dyn std::error::Error> accepts any error type.
Option<T>
fn find_host(name: &str) -> Option<&'static str> {
match name {
"sw01" => Some("10.50.1.10"),
_ => None,
}
}
// Combinators
let ip = find_host("sw01").unwrap_or("0.0.0.0");
let upper = find_host("sw01").map(|ip| ip.to_uppercase());
// ? works on Option too (in functions returning Option)
fn first_ip(names: &[&str]) -> Option<&'static str> {
let name = names.first()?; // returns None if empty
find_host(name)
}
Custom Error Types
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("config not found: {path}")]
ConfigNotFound { path: String },
#[error("invalid port: {0}")]
InvalidPort(u16),
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("parse error")]
Parse(#[from] std::num::ParseIntError),
}
fn load_config(path: &str) -> Result<Config, AppError> {
let data = fs::read_to_string(path)?; // io::Error -> AppError::Io
let port: u16 = data.trim().parse()?; // ParseIntError -> AppError::Parse
if port == 0 {
return Err(AppError::InvalidPort(port));
}
Ok(Config { port })
}
thiserror generates the Display and From implementations. #[from] enables automatic conversion with ?.
anyhow for Applications
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = fs::read_to_string("config.toml")
.context("failed to read config file")?;
let port: u16 = config.trim().parse()
.context("invalid port in config")?;
println!("port: {port}");
Ok(())
}
anyhow::Result<T> is Result<T, anyhow::Error>. .context() adds human-readable messages to the error chain. Use thiserror in libraries (callers need to match on error variants), anyhow in applications (you just want to display errors).
Unwrap and Expect
// unwrap — panics with generic message
let f = File::open("data.txt").unwrap();
// expect — panics with your message
let f = File::open("data.txt").expect("data.txt must exist");
// unwrap_or — fallback value
let port: u16 = env::var("PORT")
.unwrap_or("8080".into())
.parse()
.unwrap_or(8080);
unwrap() and expect() crash the program on error. Use them in tests, prototypes, and cases where failure is genuinely impossible. In production, use ? or combinators.
Error Conversion Pattern
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
// Now ? converts io::Error to AppError automatically
fn read(path: &str) -> Result<String, AppError> {
Ok(fs::read_to_string(path)?)
}
Implement From<ForeignError> for YourError to make ? work across error boundaries. thiserror’s `#[from] attribute generates this for you.