Rust Error Handling

Result<T, E>

The core error type
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

Propagate errors concisely
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>

Rust’s replacement for null
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

Define structured errors with thiserror
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

Quick error handling without custom types
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

Panic on error — development only
// 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

From trait enables ? across error types
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.