Phase 4Code organization

#16 Error handling

Result, ?, custom errors

Panic vs Result

Rust has two mechanisms for handling errors: panic! for unrecoverable errors and Result for recoverable ones. Most of the time, you should use Result.

panic! -- unrecoverable errors
fn main() {
    // panic! immediately stops the program
    // panic!("Something went terribly wrong!");

    // These also panic:
    let v = vec![1, 2, 3];
    // let x = v[99];  // index out of bounds -> panic

    // Use Result instead for recoverable situations
    let result = "42".parse::<i32>();
    match result {
        Ok(n) => println!("Parsed: {}", n),
        Err(e) => println!("Failed: {}", e),
    }
}

Rule of thumb: use panic! in examples, tests, or truly unrecoverable situations. Use Result in library code and production applications.

Custom error types

For real applications, you define your own error types. An enum is perfect for representing different kinds of errors:

Custom error enum
use std::fmt;

#[derive(Debug)]
enum AppError {
    IoError(std::io::Error),
    ParseError(std::num::ParseIntError),
    ConfigError(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::IoError(e) => write!(f, "I/O error: {}", e),
            AppError::ParseError(e) => write!(f, "Parse error: {}", e),
            AppError::ConfigError(msg) => {
                write!(f, "Configuration error: {}", msg)
            }
        }
    }
}

Implementing Display lets you print user-friendly error messages. Implementing Debug (via #[derive]) gives you detailed output for debugging.

The ? operator in depth

The ? operator does more than just propagate errors. It also calls From::from() to convert the error type automatically:

? with automatic conversion
use std::fs;

fn read_port_from_config() -> Result<u16, AppError> {
    // ? converts io::Error to AppError automatically
    let content = fs::read_to_string("config.txt")?;

    // ? converts ParseIntError to AppError automatically
    let port: u16 = content.trim().parse()?;

    Ok(port)
}

For this to work, you need From implementations that convert each source error into your custom error type.

Error conversion with From

The From trait tells Rust how to convert one error type into another. This is what makes ? so ergonomic:

Implementing From for error conversion
impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::IoError(error)
    }
}

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

// Now ? works seamlessly:
fn load_config() -> Result<String, AppError> {
    let content = std::fs::read_to_string("config.txt")?; // io::Error -> AppError
    let _port: u16 = content.trim().parse()?;              // ParseIntError -> AppError
    Ok(content)
}

You can chain multiple ? operators in a single function, and each one converts its error type through From:

Chaining ? operators
fn process_data() -> Result<String, AppError> {
    let raw = std::fs::read_to_string("data.txt")?;
    let count: i32 = raw.lines().count().to_string().parse()?;
    Ok(format!("Processed {} lines", count))
}

The anyhow crate

For applications (not libraries), the anyhow crate simplifies error handling dramatically. It provides a single anyhow::Error type that can hold any error:

Cargo.toml
[dependencies]
anyhow = "1"
Using anyhow
use anyhow::{Context, Result};
use std::fs;

// Result is anyhow::Result<T>, which is Result<T, anyhow::Error>
fn read_config() -> Result<String> {
    let content = fs::read_to_string("config.toml")
        .context("Failed to read config file")?;
    Ok(content)
}

fn parse_port(config: &str) -> Result<u16> {
    let port: u16 = config
        .trim()
        .parse()
        .context("Failed to parse port number")?;
    Ok(port)
}

fn main() -> Result<()> {
    let config = read_config()?;
    let port = parse_port(&config)?;
    println!("Server running on port {}", port);
    Ok(())
}

The .context() method adds human-readable messages to errors, creating a chain of context that makes debugging easier. For libraries, prefer custom error types so users can match on specific errors.

Summary

Quick reference
panic!("msg")              // Unrecoverable error (avoid in libs)
Result<T, E>               // Recoverable error

// Custom errors
#[derive(Debug)]
enum MyError { Variant(source) }
impl Display for MyError { ... }
impl From<OtherError> for MyError { ... }

// The ? operator
let val = might_fail()?;   // Propagate error, auto-convert via From

// anyhow (for applications)
use anyhow::{Context, Result};
fn foo() -> Result<T> { ... }
.context("message")?       // Add context to errors

Your turn

Try running the error handling demo with cargo run and verify with cargo check:

terminal — cargo
user@stemlegacy:~/error-handling-demo$