How to use thiserror crate

Add thiserror to Cargo.toml, derive the Error trait on your enum, and use it to handle custom errors cleanly.

The boilerplate trap

You are writing a function that parses a configuration file. It can fail because the file is missing, because the JSON is malformed, or because a value is out of range. You define an enum to represent these failures. You try to return a Result using your enum. The compiler rejects you. Your type does not implement std::error::Error.

You write the impl block. You implement std::fmt::Display to format the error message. You implement std::error::Error to link to the underlying cause. You realize you have three more error types in the crate, and each one requires the same repetitive code. Your fingers hurt. You just want to define the error variants and move on to the logic that actually matters.

thiserror deletes the boilerplate. It is a derive macro that generates the Error trait implementation for you. You describe the structure of the error and how it should display; the macro writes the trait implementations. You focus on the error taxonomy, not the glue code.

How the derive macro works

thiserror attaches to your type via #[derive(Error)]. It scans the type for attributes like #[error("...")] and #[from]. It generates the necessary impl blocks for std::fmt::Display and std::error::Error. The generated code is idiomatic and efficient. You get the full power of the standard library error traits without writing the implementation.

The macro supports several attribute forms. The most common is #[error("message")], which defines the display string. You can use format arguments like {0} to include fields from the variant. You can use #[from] to automatically implement From for inner error types, enabling the ? operator to convert them into your custom error. You can use #[error(transparent)] to expose an inner error's display and source directly, as if the wrapper didn't exist.

use thiserror::Error;

/// Represents errors that can occur during configuration loading.
#[derive(Error, Debug)]
pub enum ConfigError {
    // The error attribute defines the Display output.
    // {0} references the first field of the tuple variant.
    #[error("configuration file not found: {0}")]
    NotFound(String),

    // Struct variants use named fields in the format string.
    #[error("invalid value for key '{key}': {value}")]
    InvalidValue { key: String, value: String },
}

Stop writing impl Display blocks. The macro handles the boilerplate so you can focus on the error variants.

Minimal working example

Add thiserror to your dependencies. The crate is stable and widely used. The version 1.0 is the current standard.

[dependencies]
thiserror = "1.0"

Define an enum with error variants. Derive Error and Debug. The Debug derive is a convention. The Error trait does not require Debug, but every error type in Rust should implement Debug for logging and debugging. The thiserror macro does not enforce this, but the community expects it.

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("input is empty")]
    EmptyInput,

    #[error("value {0} is out of range")]
    OutOfRange(i32),
}

fn parse_value(input: &str) -> Result<i32, AppError> {
    if input.is_empty() {
        return Err(AppError::EmptyInput);
    }

    let value: i32 = input.parse().map_err(|_| AppError::OutOfRange(0))?;
    if value < 0 || value > 100 {
        return Err(AppError::OutOfRange(value));
    }

    Ok(value)
}

The #[error("...")] attribute maps directly to the Display implementation. When you print the error, Rust calls Display, which uses the string you provided. The {0} placeholder pulls the value from the tuple field. The macro generates the Display impl that formats the string correctly.

Convention aside: use #[derive(Error, Debug)] together. Always. If you forget Debug, you lose the ability to print the error structure in logs, which makes debugging harder. The two derives belong on the same line.

Error chaining and conversion

Real code rarely fails in isolation. A function might fail because a file read failed, or because a network request failed. You want to wrap those underlying errors in your custom error type so callers can inspect the chain of causes.

thiserror provides #[from] to automate this. When you add #[from] to a field, the macro generates a From implementation for that field's type. This allows the ? operator to convert the inner error into your custom error automatically.

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

#[derive(Error, Debug)]
enum FileError {
    // #[from] generates impl From<std::io::Error> for FileError.
    // This allows ? to convert io::Error into FileError::Io automatically.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

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

fn read_number(path: &str) -> Result<i32, FileError> {
    // The ? operator converts std::io::Error to FileError::Io via #[from].
    let content = fs::read_to_string(path)?;

    // The ? operator converts ParseIntError to FileError::Parse via #[from].
    let number = content.trim().parse()?;

    Ok(number)
}

The #[from] attribute is a massive quality-of-life improvement. Without it, you would need to write a manual impl From<std::io::Error> for FileError for every error type you want to wrap. With #[from], the macro generates the conversion logic. The ? operator works seamlessly.

You can also use #[error(transparent)] when you want the wrapper to expose the inner error completely. This is useful when you have a single-field variant that wraps another error, and you want the display message and source chain to come directly from the inner error. The wrapper becomes invisible to the user.

#[derive(Error, Debug)]
enum ApiError {
    // Transparent propagates Display and Source from the inner error.
    // The message comes from reqwest::Error, not from this attribute.
    #[error(transparent)]
    Request(#[from] reqwest::Error),
}

Use #[from] liberally. It turns the ? operator into a friend instead of a compiler error.

Formatting options

The #[error("...")] attribute supports flexible formatting. You can use positional arguments, named arguments, and even custom logic.

Positional arguments use {0}, {1}, etc., to reference tuple fields. Named arguments use {field_name} for struct variants. You can also use format specifiers like {:?} for debug formatting or {:.2} for floating-point precision.

#[derive(Error, Debug)]
enum FormatError {
    // Positional arguments for tuple variants.
    #[error("error at line {0}, column {1}")]
    Position(usize, usize),

    // Named arguments for struct variants.
    #[error("missing field '{field}' in section '{section}'")]
    MissingField { field: String, section: String },

    // Custom format specifiers.
    #[error("value {0:.2} exceeds threshold")]
    ThresholdExceeded(f64),
}

You can also use #[error("...")] with a custom display implementation by omitting the format string and providing a method. This is rare but useful when the display logic is complex.

#[derive(Error, Debug)]
enum ComplexError {
    #[error("{self}")]
    Custom {
        code: u32,
        message: String,
    },
}

impl std::fmt::Display for ComplexError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error {}: {}", self.code, self.message)
    }
}

Check your format strings. A typo in {0} vs {1} breaks compilation, and the error message points to the macro, not the typo.

Pitfalls and compiler errors

thiserror is robust, but there are common mistakes.

If you reference a field index that does not exist, the macro expansion fails. The compiler reports a macro error, which can be cryptic. The error message points to the #[derive(Error)] line, not the specific attribute. You have to inspect the attributes to find the mismatch.

#[derive(Error, Debug)]
enum BadError {
    // This fails: the variant has no fields, but the format string uses {0}.
    #[error("error: {0}")]
    NoFields,
}

The compiler rejects this with a macro expansion error. The message indicates that the format argument is missing. Fix the format string to match the variant structure.

If you use a type in the format string that does not implement Display, the compiler rejects you with E0277 (trait bound not satisfied). The error occurs during macro expansion, so the message references the generated code. Ensure all types used in {...} implement Display or use {:?} for Debug.

#[derive(Error, Debug)]
enum DisplayError {
    // This fails: MyStruct does not implement Display.
    #[error("error: {0}")]
    BadType(MyStruct),
}

struct MyStruct;

The compiler reports E0277 because MyStruct does not implement std::fmt::Display. Add #[derive(Debug)] to MyStruct and change the format string to {0:?}, or implement Display for MyStruct.

If you use #[from] on a type that does not implement Error, the macro still generates the From impl, but the resulting type might not behave as expected in error chains. The Error trait expects source() to return an Option<&(dyn Error + 'static)>. If the inner type is not an error, source() returns None. This is valid but might break expectations in code that inspects the error chain.

Convention aside: keep #[from] on types that implement Error. If you wrap a non-error type, use a plain field without #[from] and handle the conversion manually. This keeps the error chain semantically correct.

When to use thiserror

Use thiserror when you define custom error enums in a library and want to avoid boilerplate. Use thiserror when you need #[from] to convert inner errors automatically with the ? operator. Use thiserror when you want #[error(transparent)] to expose underlying error details without wrapping them. Reach for manual impl when you need custom logic in Display that the attribute syntax cannot express, which is rare. Reach for anyhow or eyre when you are writing application code and do not need to expose error types to the public API.

If you are building a library, thiserror is the standard. It produces clean error types that callers can match on. If you are writing the binary, look at anyhow for simpler error handling.

Where to go next