How to Use the thiserror Crate for Custom Errors

Use the `thiserror` crate to define custom error enums with automatic `std::error::Error` implementation via derive macros.

When errors get out of hand

You are building a tool that reads a configuration file, validates the syntax, and then fetches data from an API. Three things can go wrong. The file might be missing. The JSON might be malformed. The server might return a 500 error. In Rust, you need a single error type to represent all these failures so your function signature stays clean. You define an enum with a variant for each case.

Now you need to implement std::error::Error. That trait requires Display and Debug. You write impl Display for Error. You match every variant and format a message. You write impl Error. You write impl From<io::Error> so you can use the ? operator on file reads. You write impl From<serde_json::Error> for the parser. You add a new variant later. You update Display. You forget to update From. The ? operator breaks. The compiler screams. You fix it. You add another variant. The cycle repeats.

This is the error implementation tax. You pay it every time you define a custom error type. The code is repetitive, easy to get wrong, and distracts from the actual logic. You spend more time writing glue code than solving the problem.

The derive macro shortcut

thiserror removes the tax. It is a procedural macro that generates the trait implementations for you. You define the structure of your error enum and add attributes to the variants. The macro reads those attributes and writes the Display, Error, and From implementations.

Think of it like a specialized form-filling robot. You hand the robot a blank form with a few notes in the margins. The robot reads the notes and fills in every field correctly. You don't need to know the exact rules for every field. You just tell the robot what goes where. The robot handles the formatting, the chaining, and the type conversions. If you add a new variant, you just add a new note. The robot adapts instantly.

Minimal setup

Add thiserror to your dependencies. The version 1.0 is stable and widely used.

[dependencies]
thiserror = "1.0"

Create an enum and derive Error. You must also derive Debug. The thiserror macro enforces this because the Error trait documentation strongly recommends implementing Debug, and the macro refuses to run without it.

use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    // The error attribute defines the Display message.
    // {0} refers to the first field in the tuple variant.
    #[error("File not found: {0}")]
    NotFound(String),

    // You can format multiple fields.
    #[error("Invalid value: {0} (expected {1})")]
    InvalidValue(i32, i32),
}

The macro generates impl std::fmt::Display that matches on the variants and formats the strings. It also generates impl std::error::Error. You can use this enum anywhere an error is expected.

Define the shape. The macro handles the traits.

What the macro generates

When you compile, thiserror expands the #[derive(Error)] attribute. It inspects the enum and produces code.

For Display, it creates a match statement. Each variant gets an arm that formats the string you provided. The {0} placeholders are replaced with the fields of the variant. If you use a struct variant, you can use {field_name} instead of indices.

For Error, it implements the trait. This marks your type as an error. It also allows you to chain errors. If you mark a field with #[source], the macro implements the source() method to return that field. This links your error to an inner error. Debugging tools use the source chain to reconstruct the full sequence of failures.

If you mark a field with #[from], the macro generates a From implementation. This converts the inner type into your error variant. The ? operator relies on From. When you write file.read_to_string(&mut buf)?, the compiler looks for a From<io::Error> impl. If thiserror generated one, the conversion happens automatically. The ? operator wraps the inner error in your variant and returns early.

The macro also supports #[error(transparent)]. This is for wrapper types where the inner error is the whole story. The macro delegates Display and source to the inner error. The wrapper becomes invisible in the error chain. The user sees the inner error directly.

Real-world error types

Real applications need more than simple messages. You need to capture context, chain errors, and handle conversions.

use std::fs;
use std::io;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ConfigError {
    // Struct variants let you name fields.
    // The source attribute links the inner error.
    #[error("Failed to read config file: {source}")]
    Read {
        #[source]
        source: io::Error,
    },

    // Tuple variants work too.
    // from enables the ? operator for this specific type.
    #[error("Invalid port number: {0}")]
    InvalidPort(u16),

    // transparent delegates Display and source to the inner error.
    // from enables conversion.
    #[error(transparent)]
    Parse(#[from] serde_json::Error),
}

// Example usage showing how ? works with #[from].
fn load_config(path: &str) -> Result<String, ConfigError> {
    // fs::read_to_string returns Result<String, io::Error>.
    // The ? operator converts io::Error to ConfigError::Read
    // because thiserror generated the From impl.
    let content = fs::read_to_string(path)?;
    Ok(content)
}

The Read variant uses a struct. The source attribute tells the macro to return the io::Error from source(). The Display message includes the source text. This gives the user the high-level context ("Failed to read config") and the low-level detail ("No such file or directory").

The Parse variant uses transparent. If the JSON parser fails, the error message comes directly from serde_json. The ConfigError::Parse wrapper is skipped. This is useful when the wrapper adds no value. You are just bubbling up an error from a dependency.

Chain your errors properly. The source chain is how debugging tools reconstruct what went wrong.

Pitfalls and compiler signals

thiserror is robust, but a few patterns trip people up.

If you forget to derive Debug, the macro fails. You will see an error about the Debug trait not being implemented. Add #[derive(Debug)] to the enum. The macro requires it.

If you use #[from] on a variant, the types must match exactly. The macro generates impl From<InnerType> for YourError. If the variant holds a tuple, the inner type must be the tuple element. If you try to use ? on an error type that doesn't have a From impl, the compiler rejects it with E0277 (the trait bound From<InnerError> is not satisfied). Check your variant definition. The type in the variant must match the type you are converting.

Another common issue is using #[error(transparent)] on a variant with multiple fields. Transparent variants must hold exactly one field. The macro needs a single inner error to delegate to. If you have multiple fields, use a normal #[error("...")] message and mark one field as #[source].

The compiler also rejects mismatched format strings. If you write #[error("Value: {0}")] on a struct variant with no indexable fields, the macro complains. Use {field_name} for struct variants.

Check your Debug derive. The macro won't run without it.

When to reach for thiserror

Rust has several ways to handle errors. The right tool depends on where you are in the stack.

Use thiserror when you are writing a library or a module and need to define a custom error enum with specific variants. Use thiserror when you want to avoid writing manual Display and Error implementations while keeping full control over the error structure. Use thiserror when you need to chain errors with #[source] or convert inner errors with #[from].

Use anyhow when you are writing application code and just need a quick error type without defining a custom enum. anyhow provides a generic Result<T, anyhow::Error> that can wrap any error. It is great for main.rs and top-level functions.

Use std::io::Error when your errors are purely I/O related and you don't need application-specific variants.

Use manual implementation when you have complex formatting logic that attributes cannot express, though this is rare. Most cases are covered by thiserror.

Libraries define errors. Applications consume them. Pick the tool that matches your boundary.

Where to go next