Free 40-page Claude guide — setup, 120 prompt codes, MCP servers, AI agents. Download free →
CLSkills
RustintermediateNew

Rust Error Handling Patterns

Share

Build idiomatic error types using thiserror for libraries and anyhow for applications

Works with OpenClaude

You 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

  1. For libraries: define a custom error enum with thiserror, one variant per failure mode
  2. For applications: use anyhow::Result<T> and let errors flow up
  3. Use the ? operator to propagate errors instead of explicit match
  4. Add #[from] attribute on thiserror enum variants to auto-convert from underlying errors
  5. Add context with .context() / .with_context() when an error crosses an abstraction boundary
  6. Implement Display sensibly — error messages should be actionable
  7. 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

Quick Info

CategoryRust
Difficultyintermediate
Version1.0.0
AuthorClaude Skills Hub
rusterrorsresult

Install command:

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.