Skip to content

02 Error Handling

Goal

Handle errors idiomatically with custom error types, thiserror, anyhow, and proper error propagation.

Result and Option Recap

rust
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn divide_result(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("division by zero".into()) } else { Ok(a / b) }
}

Custom Error Types

rust
use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NegativeSquareRoot,
    Overflow,
}

impl fmt::Display for MathError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MathError::DivisionByZero => write!(f, "division by zero"),
            MathError::NegativeSquareRoot => write!(f, "cannot take square root of negative number"),
            MathError::Overflow => write!(f, "integer overflow"),
        }
    }
}

impl Error for MathError {}

fn sqrt(x: f64) -> Result<f64, MathError> {
    if x < 0.0 { Err(MathError::NegativeSquareRoot) } else { Ok(x.sqrt()) }
}

thiserror — Derive Error Implementations

toml
# Cargo.toml
[dependencies]
thiserror = "1.0"
rust
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Parse error at line {line}: {msg}")]
    Parse { line: usize, msg: String },
    
    #[error("Config error: {0}")]
    Config(#[from] config::ConfigError),
    
    #[error("Database error")]
    Database(#[source] sqlx::Error),
}

fn read_config(path: &str) -> Result<Config, AppError> {
    let content = std::fs::read_to_string(path)?; // auto-converts io::Error
    let config = toml::from_str(&content)?;
    Ok(config)
}

anyhow — Easy Error Context

toml
# Cargo.toml
[dependencies]
anyhow = "1.0"
rust
use anyhow::{Context, Result};

fn process_file(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read {}", path))?;
    
    let parsed = parse(&content)
        .context("failed to parse content")?;
    
    Ok(parsed)
}

fn parse(s: &str) -> Result<i32> {
    s.trim().parse().context("not a valid integer")
}

Error Propagation Patterns

Early Return with ?

rust
fn process() -> Result<(), AppError> {
    let data = fetch_data()?;
    let validated = validate(data)?;
    save(validated)?;
    Ok(())
}

Map and And_then

rust
fn main() {
    let result: Result<i32, _> = "42".parse();
    let doubled = result.map(|x| x * 2);
    
    let result: Result<i32, _> = "42".parse();
    let doubled = result.and_then(|x| Ok(x * 2));
}

Collecting Results

rust
fn process_all(items: Vec<String>) -> Result<Vec<i32>, ParseError> {
    items.into_iter()
        .map(|s| s.parse())
        .collect::<Result<Vec<_>, _>>()
}

Error Handling in Libraries vs Applications

ContextApproach
LibraryCustom error types with thiserror, exhaustive variants
Applicationanyhow::Error for flexibility, context chains

Checkpoint

rust
use thiserror::Error;
use anyhow::Result;

#[derive(Error, Debug)]
enum AppError {
    #[error("file not found: {0}")]
    NotFound(String),
    #[error("permission denied")]
    PermissionDenied,
    #[error(transparent)]
    Io(#[from] std::io::Error),
}

fn read_file_safe(path: &str) -> Result<String, AppError> {
    let metadata = std::fs::metadata(path)?;
    if !metadata.is_file() {
        return Err(AppError::NotFound(path.into()));
    }
    std::fs::read_to_string(path).map_err(AppError::Io)
}

fn main() -> Result<()> {
    let content = read_file_safe("config.txt")
        .context("failed to load config")?;
    println!("Config: {}", content);
    Ok(())
}

Next

Continue to 03 Generic Types.