rust-error-handling

gar-ai's avatarfrom gar-ai

Design error types using thiserror for libraries and anyhow for applications. Use when defining error hierarchies or handling fallible operations.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 30, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for Rust error handling, utilizing industry-standard crates like thiserror for structured library errors and anyhow for context-rich application errors. It helps developers design robust error hierarchies, implement automatic conversions, and follow best practices for fallible operations to ensure code reliability and maintainability.

Use Cases

  • Building a Rust library: Use thiserror to define custom, strongly-typed error enums that allow consumers to handle specific failure cases programmatically.
  • Developing a Rust application: Implement anyhow to capture high-level context and stack-like information for errors propagated from various modules or dependencies.
  • Refactoring legacy code: Transition from basic String-based errors or unwrap() calls to a structured hierarchy that improves debugging efficiency and error propagation.
  • Designing domain-specific errors: Create nested error types for different subsystems (e.g., Database, API, I/O) with clear conversion logic and retry-aware properties.
namerust-error-handling
descriptionDesign error types using thiserror for libraries and anyhow for applications. Use when defining error hierarchies or handling fallible operations.

Error Handling

Use thiserror for libraries (structured errors) and anyhow for applications (context-rich errors).

Library Pattern: thiserror

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ProcessingError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("S3 error: {0}")]
    S3(String),

    #[error("Media extraction error: {0}")]
    MediaExtraction(String),

    #[error("Python execution error: {0}")]
    Python(String),

    #[error("Configuration error: {0}")]
    Config(String),

    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
}

// Type alias for ergonomics
pub type Result<T> = std::result::Result<T, ProcessingError>;

Key thiserror Features

#[derive(Error, Debug)]
pub enum MyError {
    // Automatic From implementation
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    // Transparent wrapper (delegates Display/source)
    #[error(transparent)]
    Other(#[from] anyhow::Error),

    // Formatted message with fields
    #[error("Failed to process {path}: {reason}")]
    ProcessFailed { path: String, reason: String },

    // Source error without From
    #[error("Database query failed")]
    Query(#[source] sqlx::Error),
}

Application Pattern: anyhow

use anyhow::{Context, Result, bail, ensure};

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .context("Failed to read config file")?;

    let config: Config = serde_json::from_str(&content)
        .with_context(|| format!("Failed to parse config from {}", path))?;

    ensure!(config.is_valid(), "Invalid configuration");

    Ok(config)
}

fn process() -> Result<()> {
    if something_wrong {
        bail!("Something went wrong: {}", details);
    }
    Ok(())
}

Error Propagation

// Use ? for automatic conversion
fn process_video(path: &Path) -> Result<Video> {
    let data = std::fs::read(path)?;           // io::Error -> ProcessingError
    let video = parse_video(&data)?;           // ParseError -> ProcessingError
    Ok(video)
}

// Add context when propagating
fn load_and_process(path: &Path) -> Result<Video> {
    let video = process_video(path)
        .map_err(|e| ProcessingError::MediaExtraction(
            format!("Failed to process {}: {}", path.display(), e)
        ))?;
    Ok(video)
}

Domain-Specific Error Hierarchy

// Top-level error enum
#[derive(Error, Debug)]
pub enum AppError {
    #[error("Database error: {0}")]
    Database(#[from] DatabaseError),

    #[error("API error: {0}")]
    Api(#[from] ApiError),
}

// Sub-domain errors
#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("Connection failed: {0}")]
    Connection(String),

    #[error("Query failed: {0}")]
    Query(#[from] sqlx::Error),

    #[error("Record not found: {0}")]
    NotFound(String),
}

// Conversion between domains
impl From<DatabaseError> for ApiError {
    fn from(e: DatabaseError) -> Self {
        match e {
            DatabaseError::NotFound(id) => ApiError::NotFound(id),
            other => ApiError::Internal(other.to_string()),
        }
    }
}

Retry-Aware Errors

#[derive(Error, Debug)]
pub enum ProcessingError {
    #[error("Temporary error (retryable): {0}")]
    Temporary(String),

    #[error("Permanent error: {0}")]
    Permanent(String),
}

impl ProcessingError {
    pub fn is_retryable(&self) -> bool {
        matches!(self, ProcessingError::Temporary(_))
    }
}

Guidelines

  • Use thiserror for library code (structured, typed errors)
  • Use anyhow for application code (context-rich, flexible)
  • Always derive Debug on error types
  • Use #[from] for automatic conversion from common errors
  • Add context with .context() or .with_context()
  • Create domain-specific error variants
  • Use ? operator instead of .unwrap() or .expect()
  • Consider if errors are retryable

Examples

See hercules-local-algo/src/error.rs for production error hierarchy.