Agent skill
rust-error-handling
Rust error handling patterns and best practices. Use when designing error types, implementing error propagation, adding error context, converting between error types, or debugging error handling issues.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/rust-error-handling
SKILL.md
Rust Error Handling
Comprehensive guide to error handling in Rust projects.
Choosing the Right Approach
Library Crates: Use thiserror
use thiserror::Error;
/// Errors that can occur in this library.
#[derive(Debug, Error)]
pub enum Error {
/// Input validation failed.
#[error("invalid input: {message}")]
InvalidInput { message: String },
/// Resource was not found.
#[error("{resource_type} not found: {id}")]
NotFound {
resource_type: &'static str,
id: String,
},
/// Operation timed out.
#[error("operation timed out after {duration:?}")]
Timeout { duration: std::time::Duration },
/// Wraps IO errors.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// Wraps serialization errors.
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
/// Convenient Result alias.
pub type Result<T> = std::result::Result<T, Error>;
Application Binaries: Use anyhow
use anyhow::{anyhow, bail, ensure, Context, Result};
fn main() -> Result<()> {
let config = load_config()
.context("failed to load configuration")?;
run_app(config)
.context("application error")?;
Ok(())
}
fn load_config() -> Result<Config> {
let path = std::env::var("CONFIG_PATH")
.context("CONFIG_PATH environment variable not set")?;
let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.context("failed to parse configuration")?;
// Use ensure! for validations
ensure!(config.workers > 0, "workers must be greater than 0");
ensure!(config.port != 0, "port cannot be 0");
Ok(config)
}
fn validate_input(input: &str) -> Result<()> {
if input.is_empty() {
bail!("input cannot be empty");
}
if input.len() > 1000 {
bail!("input too long: {} bytes (max 1000)", input.len());
}
Ok(())
}
Error Design Patterns
Structured Error Data
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("field '{field}' is required")]
Required { field: &'static str },
#[error("field '{field}' must be between {min} and {max}, got {value}")]
OutOfRange {
field: &'static str,
min: i64,
max: i64,
value: i64,
},
#[error("field '{field}' has invalid format: {reason}")]
InvalidFormat {
field: &'static str,
reason: String,
},
}
// Usage
fn validate_age(age: i32) -> Result<(), ValidationError> {
if age < 0 || age > 150 {
return Err(ValidationError::OutOfRange {
field: "age",
min: 0,
max: 150,
value: age as i64,
});
}
Ok(())
}
IO Error Kinds (Rust 1.85+)
use std::io::{Error, ErrorKind};
fn handle_io_error(err: Error) {
match err.kind() {
// New in Rust 1.85
ErrorKind::QuotaExceeded => {
eprintln!("Disk quota exceeded");
}
ErrorKind::CrossesDevices => {
eprintln!("Cannot move across filesystems, will copy instead");
}
// Common kinds
ErrorKind::NotFound => {
eprintln!("File not found");
}
ErrorKind::PermissionDenied => {
eprintln!("Permission denied");
}
ErrorKind::TimedOut => {
eprintln!("Operation timed out");
}
_ => {
eprintln!("IO error: {err}");
}
}
}
Error Categorization
#[derive(Debug, Error)]
pub enum Error {
// Client errors (4xx equivalent)
#[error("validation error: {0}")]
Validation(#[from] ValidationError),
#[error("not found: {0}")]
NotFound(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
// Server errors (5xx equivalent)
#[error("internal error: {0}")]
Internal(String),
#[error("service unavailable: {0}")]
Unavailable(String),
}
impl Error {
/// Returns true if this is a client error (retrying won't help).
pub fn is_client_error(&self) -> bool {
matches!(self,
Error::Validation(_) |
Error::NotFound(_) |
Error::Unauthorized(_)
)
}
/// Returns true if this error is retryable.
pub fn is_retryable(&self) -> bool {
matches!(self, Error::Unavailable(_))
}
}
Opaque vs Transparent Wrapping
// Transparent: expose source error type
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
}
// Opaque: hide implementation details
#[derive(Debug, Error)]
pub enum Error {
#[error("storage error: {0}")]
Storage(String),
}
impl From<sqlx::Error> for Error {
fn from(e: sqlx::Error) -> Self {
// Don't expose sqlx types to consumers
Error::Storage(e.to_string())
}
}
Error Propagation
The ? Operator
fn process() -> Result<Output, Error> {
let input = read_input()?; // Propagates Error
let parsed = parse(input)?; // Propagates Error
let result = compute(parsed)?; // Propagates Error
Ok(result)
}
Converting Error Types
fn process() -> Result<Output, MyError> {
// Using From trait (via #[from] or manual impl)
let data = read_file()?; // io::Error -> MyError
// Using map_err for custom conversion
let parsed = parse(&data)
.map_err(|e| MyError::Parse(e.to_string()))?;
// Using ok_or for Option -> Result
let value = parsed.get("key")
.ok_or(MyError::MissingField("key"))?;
Ok(value)
}
Adding Context
use anyhow::Context;
fn load_workflow(id: &str) -> anyhow::Result<Workflow> {
let path = get_workflow_path(id)
.with_context(|| format!("invalid workflow id: {id}"))?;
let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read workflow file: {}", path.display()))?;
let workflow: Workflow = serde_json::from_str(&content)
.with_context(|| format!("failed to parse workflow: {id}"))?;
workflow.validate()
.with_context(|| format!("workflow validation failed: {id}"))?;
Ok(workflow)
}
// Error output:
// Error: workflow validation failed: my-workflow
//
// Caused by:
// 0: field 'steps' cannot be empty
Error Handling Patterns
Match on Error Variants
match do_operation() {
Ok(result) => use_result(result),
Err(Error::NotFound(id)) => {
log::warn!("Resource not found: {id}");
create_default()
}
Err(Error::Timeout { duration }) => {
log::error!("Operation timed out after {duration:?}");
Err(Error::Timeout { duration })
}
Err(e) => Err(e), // Propagate other errors
}
Fallback Values
// Default on error
let config = load_config().unwrap_or_default();
// Specific fallback
let port = parse_port(input).unwrap_or(8080);
// Fallback with logging
let value = compute()
.inspect_err(|e| log::warn!("Computation failed: {e}, using default"))
.unwrap_or_default();
Collecting Results
// Fail fast: stop on first error
let results: Result<Vec<_>, Error> = items
.into_iter()
.map(process_item)
.collect();
// Collect all: gather successes and failures (requires itertools)
use itertools::Itertools;
let (successes, failures): (Vec<_>, Vec<_>) = items
.into_iter()
.map(process_item)
.partition_result(); // From itertools crate
// Partition manually (no external dependencies)
let mut successes = Vec::new();
let mut failures = Vec::new();
for item in items {
match process_item(item) {
Ok(result) => successes.push(result),
Err(e) => failures.push(e),
}
}
Async Error Handling
use anyhow::{Context, Result};
async fn fetch_data(url: &str) -> Result<Data> {
let response = reqwest::get(url)
.await
.with_context(|| format!("failed to fetch {url}"))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("request failed with status {status}");
}
let data = response
.json::<Data>()
.await
.context("failed to parse response")?;
Ok(data)
}
// With timeout
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout(url: &str) -> Result<Data> {
timeout(Duration::from_secs(10), fetch_data(url))
.await
.context("request timed out")?
}
Testing Errors
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_error_on_invalid_input() {
let result = process("");
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, Error::InvalidInput { .. }));
}
#[test]
fn error_message_contains_details() {
let error = Error::NotFound {
resource_type: "workflow",
id: "test-123".into(),
};
let message = error.to_string();
assert!(message.contains("workflow"));
assert!(message.contains("test-123"));
}
#[test]
fn error_source_chain() {
let io_error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
);
let error = Error::Io(io_error);
// Check source chain
use std::error::Error as _;
assert!(error.source().is_some());
}
}
Nebula Error Conventions
- Each crate defines its own Error type - no shared error crate
- Use thiserror for all library crates
- Use anyhow only in binaries and tests
- Add context at API boundaries
- Don't expose internal dependencies in public error types
- Include actionable information in error messages
// Nebula pattern
// crates/nebula-scheduler/src/error.rs
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SchedulerError {
#[error("workflow '{workflow_id}' not found")]
WorkflowNotFound { workflow_id: String },
#[error("schedule '{schedule_id}' is invalid: {reason}")]
InvalidSchedule {
schedule_id: String,
reason: String,
},
#[error("executor error: {0}")]
Executor(#[from] nebula_executor::Error),
}
pub type Result<T> = std::result::Result<T, SchedulerError>;
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?