Beyond the Basics: Result and Option

Let’s start with a quick refresher on Rust’s basic error handling types:

Result and Option Recap

// Result: for operations that can fail
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        Err("Division by zero")
    } else {
        Ok(a / b)
    }
}

// Option: for values that might be absent
fn find_user(id: u64) -> Option<User> {
    if id == 0 {
        None
    } else {
        Some(User { id, name: "User".to_string() })
    }
}

// Using Result and Option together
fn get_user_name(id: u64) -> Result<String, &'static str> {
    find_user(id).map(|user| user.name).ok_or("User not found")
}

The ? Operator

fn process_data() -> Result<String, &'static str> {
    let data = read_file("data.txt")?;  // Returns early if Err
    let processed = process_string(&data)?;  // Returns early if Err
    Ok(processed)
}

// Works with Option too
fn first_even_number(numbers: &[i32]) -> Option<i32> {
    let first = numbers.first()?;  // Returns None if empty
    if first % 2 == 0 {
        Some(*first)
    } else {
        None
    }
}

Combining Result and Option

fn process_optional_data(data: Option<&str>) -> Result<String, &'static str> {
    // Convert Option to Result
    let data = data.ok_or("No data provided")?;
    
    // Process the data
    if data.is_empty() {
        return Err("Data is empty");
    }
    
    Ok(data.to_uppercase())
}

// Using map and map_err for transformations
fn transform_data(input: Option<&str>) -> Result<i32, String> {
    input
        .ok_or("No input provided".to_string())
        .map(|s| s.parse::<i32>())
        .map_err(|e| format!("Invalid input: {}", e))?
        .map_err(|e| format!("Parse error: {}", e))
}

Custom Error Types

For real-world applications, custom error types provide better organization and flexibility:

Basic Custom Error Type

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

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

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::IoError(e) => Some(e),
            AppError::ParseError(e) => Some(e),
            AppError::ValidationError(_) => None,
            AppError::NotFoundError(_) => None,
        }
    }
}

// Implementing From for automatic conversions
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)
    }
}

// Using the custom error type
fn read_and_parse(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?;  // IoError converted automatically
    let number = content.trim().parse::<i32>()?;   // ParseError converted automatically
    
    if number < 0 {
        return Err(AppError::ValidationError("Number cannot be negative".to_string()));
    }
    
    Ok(number)
}

Using thiserror for Deriving Error Types

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("I/O error: {0}")]
    IoError(#[from] std::io::Error),
    
    #[error("Parse error: {0}")]
    ParseError(#[from] std::num::ParseIntError),
    
    #[error("Validation error: {0}")]
    ValidationError(String),
    
    #[error("Not found: {0}")]
    NotFoundError(String),
    
    #[error("Database error: {0}")]
    DatabaseError(#[from] DatabaseError),
}

#[derive(Error, Debug)]
enum DatabaseError {
    #[error("Connection error: {0}")]
    ConnectionError(String),
    
    #[error("Query error: {0}")]
    QueryError(String),
}

// Using the custom error type
fn process_user(user_id: u64) -> Result<User, AppError> {
    let user = find_user(user_id)
        .ok_or_else(|| AppError::NotFoundError(format!("User {}", user_id)))?;
    
    if !is_valid_user(&user) {
        return Err(AppError::ValidationError("Invalid user".to_string()));
    }
    
    Ok(user)
}