Rust Crates, Error Handling: Thiserror vs Anyhow

Updated on

DISCLAIMER: Expressed views on this blog are my own.

I wondered what's the difference and when to use them


Thiserror and Anyhow are both extremely popular and useful error handling crates in Rust, but they serve fundamentally different purposes, primarily based on whether you are writing a library or an application (binary).

Breaking it Down

thiserror - For Defining Specific Error Types (Libraries)

  1. Purpose: To easily create custom, specific error types (usually enums) for your library. It helps you define what errors your library can produce.
  2. Mechanism: Uses a procedural macro (#[derive(Error)]) to automatically implement the standard std::error::Error trait and often the std::fmt::Display trait based on attributes you provide.
  3. Key Features:
    • #[error("...")]: Defines the user-facing error message (for Display). Supports embedding fields.
    • #[from]: Automatically generates From<SomeOtherError> implementations, making it easy to wrap underlying errors from other libraries or standard types (like std::io::Error).
    • #[source]: Specifies the underlying source error for error chaining/reporting.
  4. Use Case: You are writing a library (my-lib) and want to return clear, well-defined errors like MyLibError::Io(std::io::Error) or MyLibError::Parsing(ParseError). Consumers of your library can then match on these specific error variants to handle them differently.
  5. Benefit: Creates a stable, explicit error API for your library. Consumers know exactly what kind of errors to expect and can handle them precisely.

Example (thiserror)

use thiserror::Error;
use std::num::ParseIntError;
use std::io;

#[derive(Error, Debug)]
pub enum DataProcessingError {
    #[error("Failed to read data from source")] // Display impl
    IoError(#[from] io::Error), // From<io::Error> impl, source is io::Error

    #[error("Invalid number format in line: {line_content}")]
    ParseError {
        line_content: String,
        #[source] // Underlying source
        source: ParseIntError,
    },

    #[error("Configuration value missing: {key}")]
    MissingConfig { key: String },

    #[error("Unknown data processing error")]
    Unknown,
}

// Function in your library
// fn process_data() -> Result<(), DataProcessingError> {
//   let data = std::fs::read_to_string("data.txt")?; // ? uses the From<io::Error>
//   let number = data.parse::<i32>().map_err(|e| DataProcessingError::ParseError {
//       line_content: data,
//       source: e,
//   })?;
//   // ...
//   Ok(())
// }

anyhow - For Handling Diverse Errors Easily (Applications)

  1. Purpose: To make error handling and reporting in applications (binaries) simpler, especially when dealing with many different error types from various libraries. It focuses on consuming errors, not defining library-specific ones.
  2. Mechanism: Provides a single, concrete error type anyhow::Error which is essentially a wrapper around any type that implements std::error::Error. It uses "type erasure" – it hides the original error type inside. It also provides the Context extension trait.
  3. Key Features:
    • anyhow::Error: A single error type you can return from functions in your application (like main).
    • ? Operator: Works seamlessly. Any Result<T, E: Error + ...> can be converted into Result<T, anyhow::Error> using ?.
    • Context Trait (use anyhow::Context;): Allows easily adding descriptive context to errors as they propagate up the call stack (result.context("Failed to read user config")?).
    • Backtraces: Can capture and display backtraces (requires environment variable setup).
  4. Use Case: You are writing an application (my-app) that calls functions from std::fs, reqwest, serde_json, and my-lib. You don't want to define dozens of From implementations or huge match statements in your main function just to handle all possible errors. You want to propagate errors easily with ? and add context where needed.
  5. Benefit: Massively reduces boilerplate error handling code in applications. Makes adding context trivial. Provides good error reports with context and potential backtraces.

Example (anyhow)

use anyhow::{Context, Result}; // Use anyhow's Result alias

// Assume DataProcessingError from the thiserror example exists in `my_lib`
// use my_lib::DataProcessingError;

fn load_and_process() -> Result<()> { // Returns anyhow::Result
    let config_data = std::fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?; // Add context with anyhow

    let user_data = reqwest::blocking::get("http://example.com/users")
        .context("Failed to fetch user data")?
        .text()
        .context("Failed to decode user data response")?;

    // Call library function that uses thiserror
    // my_lib::process_data().context("Failed during core data processing")?; // ? converts DataProcessingError to anyhow::Error

    println!("Processed successfully!");
    Ok(())
}

fn main() {
    if let Err(e) = load_and_process() {
        // anyhow::Error provides a nice display format including context and source chain
        eprintln!("Error: {:?}", e); // Example: "Error: Failed during core data processing: Failed to read data from source: No such file or directory (os error 2)"
        // For user-facing errors, use Display: eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}

Summary:

Feature thiserror anyhow
Primary Use Defining error types Handling errors
Target Libraries Applications (Binaries)
Goal Create specific, typed errors for library API Simplify error propagation and add context easily
Mechanism #[derive(Error)] macro anyhow::Error type, Context trait
Result Type Result<T, MySpecificErrorEnum> Result<T, anyhow::Error> (or anyhow::Result<T>)
Flexibility Less flexible handling (requires matching) Highly flexible handling (via ?, type erasure)
API Stability Good for library APIs Generally not for library public APIs

You often use them together: Libraries use thiserror to define their errors, and applications use anyhow to easily handle errors from those libraries (and others) without excessive boilerplate. The ? in an anyhow-using application seamlessly converts a thiserror-defined error into an anyhow::Error.

You just read "Rust Crates, Error Handling: Thiserror vs Anyhow". Please share if you liked it!
You can read more recent posts here.