Skip to content

07 Error Handling

Goal

Handle errors with Result<T, E>, Option<T>, custom error types, and the ? operator.

Result<T, E>

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

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

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Option<T>

rust
enum Option<T> {
    None,
    Some(T),
}

fn find_user(id: u32) -> Option<String> {
    if id == 1 { Some("Alice".to_string()) }
    else { None }
}

fn main() {
    match find_user(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("Not found"),
    }
}

The ? Operator

rust
use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?; // Returns Err if error
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// In main (which returns Result)
fn main() -> Result<(), std::io::Error> {
    let contents = read_file("hello.txt")?;
    println!("{}", contents);
    Ok(())
}

? with Different Error Types

rust
use std::fs::File;
use std::io::{self, Read};

fn read_username() -> Result<String, io::Error> {
    let mut file = File::open("user.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

Combining Result and Option

rust
fn main() {
    let x = Some(5);
    let y = Some(10);
    
    // and_then for Option
    let result = x.and_then(|a| y.map(|b| a + b));
    println!("{:?}", result); // Some(15)
    
    // and_then for Result
    let result = Ok(5).and_then(|a| Ok(a * 2));
    println!("{:?}", result); // Ok(10)
}

Custom Error Types

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

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Custom(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::Custom(s) => write!(f, "Error: {}", s),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            AppError::Custom(_) => None,
        }
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

thiserror — Derive Error

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

#[derive(Error, Debug)]
enum DataStoreError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    #[error("Key not found: {key}")]
    NotFound { key: String },
    
    #[error("Invalid data: {0}")]
    InvalidData(String),
    
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

anyhow — Application Errors

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

fn read_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    Ok(content)
}

fn parse_config(content: &str) -> Result<Config> {
    toml::from_str(content)
        .context("Failed to parse config")
}

fn main() -> Result<()> {
    let config = read_config()
        .and_then(|c| parse_config(&c))?;
    println!("Config loaded: {:?}", config);
    Ok(())
}

Error Handling Patterns

Pattern 1: Propagate with ?

rust
fn process() -> Result<(), MyError> {
    let data = read_file()?;
    let parsed = parse(&data)?;
    save(parsed)?;
    Ok(())
}

Pattern 2: Convert Errors

rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = File::open("data.txt")?; // Box<dyn Error>
    Ok(())
}

Pattern 3: Handle Specifically

rust
fn main() {
    match read_config() {
        Ok(config) => println!("Config: {:?}", config),
        Err(AppError::NotFound { key }) => println!("Missing: {}", key),
        Err(AppError::Io(e)) => eprintln!("IO: {}", e),
        Err(e) => eprintln!("Other: {}", e),
    }
}

Pattern 4: Recover with unwrap_or

rust
fn main() {
    let value = risky_operation().unwrap_or_else(|_| default_value());
    let value = risky_operation().unwrap_or(default_value);
}

expect vs unwrap

rust
fn main() {
    // Panic with custom message
    let f = File::open("file.txt").expect("Failed to open file.txt");
    
    // Panic with default message
    let f = File::open("file.txt").unwrap();
    
    // Better: use ? in functions returning Result
}

Checkpoint

rust
use std::fs::File;
use std::io::{self, Read};

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    EmptyFile,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::EmptyFile => write!(f, "File is empty"),
        }
    }
}

impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}

fn read_first_line(path: &str) -> Result<String, AppError> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    
    let first_line = contents.lines().next()
        .ok_or(AppError::EmptyFile)?
        .to_string();
    
    Ok(first_line)
}

fn main() -> Result<(), AppError> {
    let line = read_first_line("data.txt")?;
    println!("First line: {}", line);
    Ok(())
}

Next

Continue to 08 Testing.