Skip to main content

thiserror & anyhow

CrateBest forError type style
thiserrorLibrariesStrongly typed, structured errors
anyhowApplicationsFlexible, dynamic, context-rich errors

Think of it like this:

  • Libraries → use thiserror
  • Applications / binaries → use anyhow

thiserror — Ergonomic custom error types

Writing custom error types manually requires:

  • Implementing Display
  • Implementing Error
  • Implementing From for wrapped errors

thiserror generates all of that for you using derive macros.

Example: Library-style error with thiserror

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

#[derive(Debug, Error)]
pub enum ConfigError {
#[error("I/O error while reading config: {0}")]
Io(#[from] io::Error),

#[error("Invalid number in config: {0}")]
Parse(#[from] ParseIntError),

#[error("Missing required field: {0}")]
MissingField(String),
}

pub fn read_config_number(path: &str) -> Result<i32, ConfigError> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()?;
Ok(number)
}
  • #[derive(Error)] automatically:
    • Implements std::error::Error
    • Implements Display using #[error(...)]
    • Implements From for variants marked with #[from]
  • Callers can match on ConfigError variants.

anyhow — Flexible error handling for applications

In applications:

  • You often don’t care exactly which error type occurred.
  • You want:
    • Easy error propagation
    • Rich context
    • Good backtraces
    • Minimal boilerplate

anyhow provides a single error type: anyhow::Error.

Example: Application-style error handling with anyhow

use anyhow::{Context, Result};

fn read_number(path: &str) -> Result<i32> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file '{}'", path))?;

let number = contents
.trim()
.parse::<i32>()
.context("Failed to parse number from file")?;

Ok(number)
}

fn main() -> Result<()> {
let n = read_number("number.txt")?;
println!("Number: {}", n);
Ok(())
}
  • Result<T> is shorthand for Result<T, anyhow::Error>.
  • .context(...) and .with_context(...) add helpful messages.
  • Errors automatically carry:
    • The original cause
    • A context chain
    • A backtrace (when enabled)

Using thiserror + anyhow together

This is very common:

  • Library code uses thiserror.
  • Application code uses anyhow and converts library errors automatically.

Example: Library + App integration

Library crate:

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

#[derive(Debug, Error)]
pub enum DataError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),

#[error("Parse error: {0}")]
Parse(#[from] ParseIntError),
}

pub fn read_data(path: &str) -> Result<i32, DataError> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()?;
Ok(number)
}

Application crate:

use anyhow::Result;
use mylib::read_data;

fn main() -> Result<()> {
let value = read_data("data.txt")?;
println!("Value: {}", value);
Ok(())
}
  • DataError automatically converts into anyhow::Error.
  • You get structured errors in the library and flexible handling in the app.

Key Differences

Featurethiserroranyhow
Error typeYour own enum/structanyhow::Error
Best forLibrariesApplications
Error matchingYes (match on variants)No (opaque type)
Context messagesManualBuilt-in (context, with_context)
BacktracesVia std / featureBuilt-in support
BoilerplateLowVery low

When not to use anyhow

Avoid anyhow in:

  • Public library APIs
  • Code where callers must distinguish error types
  • Low-level libraries

Use thiserror instead.

Summary

  • thiserror:
    • Use for defining clean, typed, structured error enums.
    • Ideal for libraries and reusable modules.
  • anyhow:
    • Use for application-level error handling.
    • Provides easy propagation, context, and backtraces.
  • Together, they give you:
    • Strong typing internally
    • Flexible error handling at the top level