rust-error-handling
Design error types using thiserror for libraries and anyhow for applications. Use when defining error hierarchies or handling fallible operations.
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.
| name | rust-error-handling |
|---|---|
| description | Design 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
thiserrorfor library code (structured, typed errors) - Use
anyhowfor application code (context-rich, flexible) - Always derive
Debugon 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.