Rust Notes #7: Understanding thiserror Attributes

rust
Author

Jerry Wu

Published

July 18, 2026

Rust Notes 7

thiserror provides a convenient way to remove repetitive boilerplate when implementing custom error types.

Under the hood, thiserror mainly generates implementations related to three standard Rust traits:

The most commonly used attributes are:

Attribute Purpose Rough equivalent
#[derive(Error)] Implements std::error::Error impl Error for MyError
#[error("...")] Defines the error message impl Display for MyError
#[from] Enables automatic error conversion with ? impl From<T> for MyError
#[source] Marks the underlying cause of an error Error::source() returning Some(source)
#[error(transparent)] Wraps an error while preserving its representation Forward Display and delegate representation to the underlying error

For example:

Using thiserror

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

#[derive(Debug, Error)]
enum AppError {
    // #[from] provides From<ParseIntError> and establishes the source relationship.
    #[error("invalid port number")]
    NotNumber(#[from] ParseIntError),

    #[error("port {0} is not allowed")]
    InvalidPort(u16),

    // #[source] explicitly defines io::Error as the source.
    #[error("failed to load configuration")]
    Config {
        #[source]
        source: io::Error,
    },

    // #[error(transparent)] delegates representation to io::Error.
    // #[from] provides From<io::Error> and establishes the source relationship.
    #[error(transparent)]
    Io(#[from] io::Error),
}

Roughly equivalent handwritten implementation

use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    NotNumber(ParseIntError),
    InvalidPort(u16),
    Config { source: io::Error },
    Io(io::Error),
}

// Equivalent to #[error("...")]
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotNumber(_) => {
                write!(f, "invalid port number")
            }

            Self::InvalidPort(port) => {
                write!(f, "port {} is not allowed", port)
            }

            Self::Config { .. } => {
                write!(f, "failed to load configuration")
            }

            // Equivalent to #[error(transparent)]
            Self::Io(err) => err.fmt(f),
        }
    }
}

// Equivalent to the Error implementation generated by:
// #[derive(Error)] together with #[source] and #[from].
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            // #[from] implies #[source]
            Self::NotNumber(err) => Some(err),

            Self::InvalidPort(_) => None,

            // Explicit #[source]
            Self::Config { source } => Some(source),

            // #[from] implies #[source]
            // #[error(transparent)] delegates representation to the inner error.
            Self::Io(err) => Some(err),
        }
    }
}

// Generated by #[from] on the ParseIntError variant.
impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        Self::NotNumber(err)
    }
}

// Generated by #[from] on the io::Error variant.
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        Self::Io(err)
    }
}
WarningDisclaimer

This post was drafted by me, with AI assistance to refine the content.