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)
- Purpose: To easily create custom, specific error types (usually enums) for your library. It helps you define what errors your library can produce.
- Mechanism: Uses a procedural macro (
#[derive(Error)]
) to automatically implement the standardstd::error::Error
trait and often thestd::fmt::Display
trait based on attributes you provide. - Key Features:
#[error("...")]
: Defines the user-facing error message (forDisplay
). Supports embedding fields.#[from]
: Automatically generatesFrom<SomeOtherError>
implementations, making it easy to wrap underlying errors from other libraries or standard types (likestd::io::Error
).#[source]
: Specifies the underlying source error for error chaining/reporting.
- Use Case: You are writing a library (
my-lib
) and want to return clear, well-defined errors likeMyLibError::Io(std::io::Error)
orMyLibError::Parsing(ParseError)
. Consumers of your library can thenmatch
on these specific error variants to handle them differently. - 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)
- 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.
- Mechanism: Provides a single, concrete error type
anyhow::Error
which is essentially a wrapper around any type that implementsstd::error::Error
. It uses "type erasure" – it hides the original error type inside. It also provides theContext
extension trait. - Key Features:
anyhow::Error
: A single error type you can return from functions in your application (likemain
).?
Operator: Works seamlessly. AnyResult<T, E: Error + ...>
can be converted intoResult<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).
- Use Case: You are writing an application (
my-app
) that calls functions fromstd::fs
,reqwest
,serde_json
, andmy-lib
. You don't want to define dozens ofFrom
implementations or hugematch
statements in yourmain
function just to handle all possible errors. You want to propagate errors easily with?
and add context where needed. - 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
.