Error handling rust
Skill VersoXBT/claude-initial-setup/skills/rust/error-handling-rust
75 skills, 14 agents, 15 commands for Claude Code. Plug-and-play starter kit covering TypeScript, Python, Go, Rust, React, Next.js, FastAPI, Django, Docker, CI/CD, security, testing, and more. Cross-AI support for Cursor, Copilot, and Codex.
npx -y skills add VersoXBT/claude-initial-setup --skill error-handling-rustAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Implement idiomatic Rust error handling with Result, Option, and the ? operator. Use when the user handles errors in Rust, creates custom error types, uses thiserror or anyhow, implements the From trait for error conversion, or asks about Result/Option patterns and error composition strategies.
SKILL.md
7.6 KB, as published. Nobody here has run it
Rust Error Handling
Handle errors idiomatically in Rust using Result, Option, the ? operator,
and well-structured custom error types for robust, composable error propagation.
When to Use
- Designing error types for a library or application
- Propagating errors with the ? operator
- Converting between error types with From
- Choosing between thiserror (libraries) and anyhow (applications)
- Handling Option and Result in method chains
Core Patterns
Pattern 1: Result and the ? Operator
Use Result<T, E> for operations that can fail. The ? operator unwraps
success or returns the error early.
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<Config, io::Error> {
let content = fs::read_to_string(path)?; // returns Err early if fails
let config: Config = serde_json::from_str(&content)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(config)
}
// Chain multiple fallible operations
fn process_file(path: &str) -> Result<Summary, AppError> {
let content = fs::read_to_string(path)?; // io::Error -> AppError via From
let data = parse_data(&content)?; // ParseError -> AppError via From
let summary = analyze(&data)?; // AnalyzeError -> AppError via From
Ok(summary)
}
Pattern 2: Custom Error Types with thiserror
Use thiserror for library error types with automatic Display and From implementations.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("item not found: {id}")]
NotFound { id: String },
#[error("duplicate key: {key}")]
DuplicateKey { key: String },
#[error("connection failed after {attempts} attempts")]
ConnectionFailed { attempts: u32 },
#[error("serialization error")]
Serialization(#[from] serde_json::Error),
#[error("I/O error")]
Io(#[from] std::io::Error),
}
// Usage
fn get_item(id: &str) -> Result<Item, StorageError> {
let data = fs::read_to_string(format!("data/{id}.json"))?; // auto-converts io::Error
let item: Item = serde_json::from_str(&data)?; // auto-converts serde error
Ok(item)
}
Pattern 3: anyhow for Applications
Use anyhow in application code where you need flexible error context
without defining custom types for every error.
use anyhow::{Context, Result, bail, ensure};
fn load_config(path: &str) -> Result<Config> {
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.context("invalid TOML in config file")?;
ensure!(config.port > 0, "port must be positive, got {}", config.port);
if config.workers == 0 {
bail!("worker count must be at least 1");
}
Ok(config)
}
// anyhow errors display the full context chain:
// "failed to read config from app.toml: No such file or directory (os error 2)"
Pattern 4: Option Handling
Use Option<T> for values that may be absent. Combine with combinators
for clean, chainable logic.
fn find_user_email(users: &HashMap<u64, User>, id: u64) -> Option<String> {
users
.get(&id) // Option<&User>
.filter(|u| u.is_active) // None if not active
.and_then(|u| u.email.as_deref()) // Option<&str>
.map(|e| e.to_lowercase()) // Option<String>
}
// Convert Option to Result with context
fn get_required_header(headers: &HeaderMap, name: &str) -> Result<&str, AppError> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| AppError::MissingHeader(name.to_string()))
}
// unwrap_or, unwrap_or_default, unwrap_or_else
let port = config.port.unwrap_or(8080);
let name = user.nickname.unwrap_or_default(); // uses Default trait
let conn = pool.get().unwrap_or_else(|| create_new_connection());
Pattern 5: Implementing From for Error Conversion
The ? operator uses From to convert errors. Implement it manually when
thiserror's #[from] is not flexible enough.
#[derive(Debug)]
pub enum AppError {
Database(String),
Validation(String),
External(Box<dyn std::error::Error + Send + Sync>),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Database(msg) => write!(f, "database error: {msg}"),
AppError::Validation(msg) => write!(f, "validation error: {msg}"),
AppError::External(err) => write!(f, "external error: {err}"),
}
}
}
impl std::error::Error for AppError {}
impl From<sqlx::Error> for AppError {
fn from(err: sqlx::Error) -> Self {
AppError::Database(err.to_string())
}
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::External(Box::new(err))
}
}
Pattern 6: Error Composition Across Layers
Structure errors by layer: domain errors at the core, infrastructure errors at the edges, conversion between layers at boundaries.
// Domain layer -- pure business errors
#[derive(Debug, Error)]
pub enum DomainError {
#[error("insufficient funds: need {required}, have {available}")]
InsufficientFunds { required: u64, available: u64 },
#[error("account {0} is frozen")]
AccountFrozen(String),
}
// Infrastructure layer -- wraps external crate errors
#[derive(Debug, Error)]
pub enum InfraError {
#[error("database error")]
Db(#[from] sqlx::Error),
#[error("cache error")]
Cache(#[from] redis::RedisError),
}
// Application layer -- combines both
#[derive(Debug, Error)]
pub enum AppError {
#[error(transparent)]
Domain(#[from] DomainError),
#[error(transparent)]
Infra(#[from] InfraError),
}
// Each layer uses ? with its own error type.
// Boundaries convert with From implementations.
Anti-Patterns
-
Using
unwrap()in production code -- Panics on None/Err. Use?,expect(), or combinators. Reserveunwrap()for tests and provably-safe cases. -
Stringly-typed errors --
Result<T, String>loses type information. Define proper error enums even for simple cases.// BAD fn parse(s: &str) -> Result<Data, String> { Err(format!("invalid: {s}")) } // GOOD fn parse(s: &str) -> Result<Data, ParseError> { ... } -
Ignoring errors with
let _ =-- Explicitly handle or log. If truly ignorable, add a comment explaining why. -
Box<dyn Error>in library code -- Libraries should expose concrete error types so consumers can match on variants. UseBox<dyn Error>only in application code or useanyhow. -
Mixing thiserror and anyhow in libraries -- Use thiserror for public error types in libraries; anyhow is for application-level convenience.
Quick Reference
| Crate | Use In | Purpose |
|---|---|---|
| thiserror | Libraries | Derive Error, Display, From |
| anyhow | Applications | Flexible context, bail!, ensure! |
| Method | Purpose |
|---|---|
? | Propagate error, convert via From |
.context("msg") | Add context (anyhow) |
.map_err(fn) | Transform error type |
.ok_or(err) | Option -> Result |
.ok_or_else(fn) | Option -> Result (lazy) |
.unwrap_or(val) | Provide default for Option/Result |
Rule of thumb: libraries use thiserror, applications use anyhow.