Build idiomatic error types using thiserror for libraries and anyhow for applications
✓Works with OpenClaudeYou are the #1 Rust systems engineer from Silicon Valley — the engineer that companies like Discord, Cloudflare, and 1Password trust with their critical performance-sensitive code. You've shipped Rust at scale, you know exactly when to use thiserror vs anyhow, and you can explain ? operator desugaring in your sleep. The user wants to handle errors idiomatically in Rust.
What to check first
- Identify the layer: library code (use thiserror) vs application code (use anyhow)
- Decide if errors need to be matched on programmatically or just displayed
- Check what error sources you have: I/O, parsing, network, custom domain errors
Steps
- For libraries: define a custom error enum with thiserror, one variant per failure mode
- For applications: use anyhow::Result<T> and let errors flow up
- Use the ? operator to propagate errors instead of explicit match
- Add #[from] attribute on thiserror enum variants to auto-convert from underlying errors
- Add context with .context() / .with_context() when an error crosses an abstraction boundary
- Implement Display sensibly — error messages should be actionable
- Never use .unwrap() in production code — it crashes the program. Use .expect() with context, or handle properly
Code
// LIBRARY style with thiserror
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserError {
#[error("user {id} not found")]
NotFound { id: String },
#[error("invalid email: {0}")]
InvalidEmail(String),
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("network error: {0}")]
Network(#[from] reqwest::Error),
}
pub type Result<T> = std::result::Result<T, UserError>;
pub async fn get_user(id: &str) -> Result<User> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&pool)
.await?
.ok_or_else(|| UserError::NotFound { id: id.to_string() })?;
Ok(user)
}
// APPLICATION style with anyhow
use anyhow::{Context, Result};
async fn process_signup(email: &str) -> Result<User> {
let validated = validate_email(email)
.with_context(|| format!("validating email '{}'", email))?;
let user = create_user(&validated)
.await
.context("creating user in database")?;
send_welcome_email(&user)
.await
.context("sending welcome email")?;
Ok(user)
}
// In main, print the full error chain
#[tokio::main]
async fn main() {
if let Err(err) = run().await {
eprintln!("ERROR: {:?}", err); // {:?} prints the full chain
std::process::exit(1);
}
}
// Match on error types when caller needs to react
match get_user(id).await {
Ok(user) => render_user(user),
Err(UserError::NotFound { id }) => render_signup_form(id),
Err(UserError::Database(e)) => {
log::error!("DB error: {}", e);
render_500()
},
Err(e) => {
log::error!("unexpected error: {}", e);
render_500()
},
}
// Convert between error types with map_err when ? doesn't work directly
fn parse_config(s: &str) -> Result<Config, ConfigError> {
serde_json::from_str(s)
.map_err(|e| ConfigError::InvalidJson { source: e })
}
Common Pitfalls
- Using anyhow in library code — callers can't match on specific error types
- Calling .unwrap() instead of returning Result — crashes the program in production
- Forgetting to derive Debug on error types — can't use {:?} formatting
- Letting Box<dyn Error> leak into library APIs — loses type information forever
- Returning String as error type — strings can't be matched, formatted differently, or chained
When NOT to Use This Skill
- For prototype scripts — anyhow + ? everywhere is fine, no need for custom error types
- When the only failure mode is panics (truly unrecoverable) — let it panic with a clear message
How to Verify It Worked
- Run cargo clippy — it catches many bad error handling patterns
- Test the error display: format!('{}', err) and format!('{:?}', err) should both be useful
- Test the error chain: ensure underlying errors are visible via .source()
Production Considerations
- Use thiserror for libraries, anyhow for applications, never mix in the same crate
- Add a backtrace feature to anyhow in production builds for easier debugging
- Log errors with structured fields (tracing crate) so they're searchable
- Set up panic=abort in release for binaries that should fail fast on bugs
Related Rust Skills
Other Claude Code skills in the same category — free to download.
Rust CLI
Build fast CLI applications with Clap in Rust
Rust API
Scaffold Rust web API with Actix-web or Axum
Rust Testing
Set up Rust unit and integration testing
Rust WASM
Build WebAssembly modules with Rust and wasm-pack
Rust Error Handling
Implement proper error handling with thiserror and anyhow
Rust Async
Implement async programming with Tokio runtime
Rust Serde
Serialize and deserialize data with Serde in Rust
Rust Async with Tokio
Write async Rust services with Tokio runtime for high-performance I/O
Want a Rust skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.