thiserror (Rust): #[from] & vs anyhow Guide 2026

Max WellsMax WellsFounder of Rustify

TL;DR: thiserror is a procedural macro crate that generates the boilerplate for custom error types: impl std::error::Error, impl Display, and impl From<OtherError>. You write a single #[derive(Error)] enum with #[error("...")] messages; thiserror handles everything else. It is the standard choice for library crates that need typed, structured errors. For application code where you just want to propagate errors without defining types, use anyhow instead.


What Is thiserror?

thiserror lets you define custom Rust error types by annotating enums or structs with #[derive(Error)] and #[error("...")]; eliminating ~50 lines of boilerplate per error type.

# Cargo.toml
[dependencies]
thiserror = "2"
use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum AppError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
 
    #[error("user not found: id={id}")]
    UserNotFound { id: u64 },
 
    #[error("invalid input: {message}")]
    InvalidInput { message: String },
 
    #[error("unauthorized")]
    Unauthorized,
}

This generates:

  • impl std::fmt::Display for AppError (from #[error("...")])
  • impl std::error::Error for AppError
  • impl From<sqlx::Error> for AppError (from #[from])

How Do You Use #[from] to Convert Errors?

#[from] generates a From<T> impl so the ? operator automatically converts upstream errors into your error type.

use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum ServiceError {
    #[error("database error: {0}")]
    Db(#[from] sqlx::Error),
 
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
 
    #[error("parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),
}
 
async fn get_user_count(pool: &sqlx::PgPool) -> Result<i64, ServiceError> {
    let count = sqlx::query_scalar!("SELECT COUNT(*) FROM users")
        .fetch_one(pool)
        .await?;  // sqlx::Error → ServiceError::Db via From
    Ok(count.unwrap_or(0))
}

The ? operator calls From::from(err) automatically; #[from] makes this work without manual impl blocks.


How Do You Format Error Messages?

The #[error("...")] string supports named fields, positional {0}, and any Display-able expression.

#[derive(Error, Debug)]
pub enum ParseError {
    // Named field from struct variant
    #[error("expected {expected}, got {got} at line {line}")]
    UnexpectedToken { expected: String, got: String, line: usize },
 
    // Positional; tuple variant field 0
    #[error("invalid number: {0}")]
    InvalidNumber(String),
 
    // Access source error via {0} or named
    #[error("config file error: {source}")]
    Config { #[from] source: std::io::Error },
 
    // Static message
    #[error("end of file reached unexpectedly")]
    UnexpectedEof,
}

What Is the Difference Between thiserror and anyhow?

thiserror is for defining typed errors (library code). anyhow is for erasing error types into a single dynamic error (application code).

thiserroranyhow
PurposeDefine structured error typesPropagate any error easily
Output typeYourError enum/structanyhow::Error (type-erased)
Pattern matching✅ Can match on variants❌ Not easily
Best forLibraries, domain errorsBinaries, glue code
BoilerplateLow (derive macro)Near-zero

Use thiserror in a library so callers can handle specific error cases. Use anyhow in main.rs or CLI code where you just want to display and exit.


Frequently Asked Questions

Yes. #[derive(Error)] works on both enums and structs. Structs are useful when a module only has one error kind:

#[derive(Error, Debug)]
#[error("connection refused at {address}: {source}")]
pub struct ConnectionError {
    address: String,
    #[source]
    source: std::io::Error,
}

#[from] implies #[source] and also generates a From impl. #[source] only marks the field as the error source (for Error::source()) without generating From. Use #[source] when you want to expose the cause but need manual conversion.

thiserror v2 supports no_std with the no-std feature flag. The std::error::Error trait requires nightly in no_std, but thiserror can generate Display and the struct/enum itself without it.

Always use thiserror; the manual approach is more verbose and error-prone. The generated code is identical to what you would write by hand.


Sources


  • anyhow: Companion crate for propagating errors without defining types
  • Result: The Result<T, E> type that error types plug into
  • Error Handling: Rust's broader error handling philosophy
  • Try Operator: The ? operator that #[from] powers
  • Display/Debug: thiserror generates Display output that makes custom error messages readable
  • From / Into: #[from] derives the conversion impls that integrate custom errors into ?

Keep Reading

Ready to Land a $80-120k Rust Job?