Skip to content

SOP: Error Handling ​

Updated Mar 2026

Overview ​

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 ​

ScenarioUse
Function can fail, caller handles itResult<T, E>
Value might not existOption<T>
Bug in logic, should never happenpanic!() / unreachable!()
Library with specific error variantsthiserror custom enum
Application, error detail mattersanyhow::Result with .context()
Quick prototype / test.unwrap() / .expect("msg")
Multiple error types, no custom enumBox<dyn std::error::Error>

Anti-Patterns ​

Do NOTDo Instead
.unwrap() in production pathsUse ? or pattern match
Ignore Result with let _ =Handle or explicitly document why
panic!() for expected failuresUse Result and propagate
Catch-all Box<dyn Error> in librariesDefine specific error types
Nested match on Result/OptionChain .map(), .and_then(), ?

Built with VitePress | Rust SOP Documentation