# thiserror: Rust error enums that carry their own messages

> A derive macro that writes the Display, From, source and backtrace plumbing for your error type, so the errors you design by hand behave the same as the ones the macro writes.

**dtolnay/thiserror** — derive(Error) for struct and enum error types

- Repository: https://github.com/dtolnay/thiserror
- Stars: 5,550 · Forks: 214
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/dtolnay-thiserror

## Adding the dependency and writing the enum

Installation is the standard Cargo line:

```toml
[dependencies]
thiserror = "2"
```

The example that follows in the README is an enum with four variants that between them cover the shapes the crate supports: a wrapped standard library error with `#[from]`, a tuple variant carrying a `String`, a struct variant with named fields that interpolates two of them, and a unit variant with a fixed message:

```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}
```

Note what is absent. There is no trait implementation block, no manual `Display`, and no hand-written `From`. Those are the parts that would make up roughly forty lines per error type in a typical handwritten implementation, and they are exactly what the derive macro generates.

## The macro stays out of your public API

The README's first bullet under Details is the one that matters most for anyone publishing a library, and it is a deliberate architectural choice rather than a side effect. thiserror deliberately does not appear in your public API: you get the same thing as if you had written an implementation of `std::error::Error` by hand, and switching from handwritten impls to thiserror, or the other way round, is not a breaking change.

That property has a concrete consequence. If a downstream user depends on your crate and you later swap your handwritten impls for the derive macro, their compiler does not break, because what they see is trait implementations either way. The same reasoning extends to a version bump: consumers are not forced to add a dependency they never asked for.

The macro supports every error shape the standard library trait allows. Errors may be enums, structs with named fields, tuple structs, or unit structs, so it does not push you toward a house style. The generated `Display` impl appears when you supply `#[error(...)]` messages, and interpolation shorthands cover both named and positional fields.

## Interpolation shorthands and named format arguments

The message syntax is a small language worth knowing, because most of what people look up is really about how to refer to a field. The README lists four shorthands: `#[error("{var}")]` behaves like writing the field directly, `#[error("{0}")]` refers to a tuple field by index, and both have debug-format counterparts with a `:?` suffix.

These can be combined with extra format arguments that may be arbitrary expressions, which is how you do something that neither shorthand covers on its own:

```rust
#[derive(Error, Debug)]
pub enum Error {
    #[error("invalid rdo_lookahead_frames {0} (expected < {max})", max = i32::MAX)]
    InvalidLookahead(u32),
}
```

When an additional expression needs to reach a field of the type, the convention is that named fields are referred to as `.var` and tuple fields as `.0`, with a leading dot:

```rust
#[derive(Error, Debug)]
pub enum Error {
    #[error("first letter must be lowercase but was {:?}", first_char(.0))]
    WrongCase(String),
    #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)]
    OutOfBounds { idx: usize, limits: Limits },
}
```

The second example is the interesting one, because it shows the interpolation syntax reaching into a nested struct field rather than only the top level of the variant.

## From, source and the rules between them

A `From` impl is generated for each variant containing a `#[from]` attribute, which is how a standard library error gets converted into your type without you writing the conversion:

```rust
#[derive(Error, Debug)]
pub enum MyError {
    Io(#[from] io::Error),
    Glob(#[from] globset::Error),
}
```

There are constraints, and they are stated plainly. The variant using `#[from]` must not contain any other fields beyond the source error and possibly a backtrace, and although `#[from]` fields are usually unnamed it is also allowed on a named field.

The Error trait's `source()` method is implemented to return whichever field carries a `#[source]` attribute or is simply named `source`. Since `#[from]` always implies `#[source]`, you never need to write both. Any error type implementing `std::error::Error`, or that dereferences to `dyn std::error::Error`, works as a source:

```rust
#[derive(Error, Debug)]
pub struct MyError {
    msg: String,
    #[source]  // optional if field name is `source`
    source: anyhow::Error,
}
```

That last example is also a reminder that thiserror composes with anyhow rather than competing with it. Naming the field `source` removes the need for the attribute entirely, which is the tidier form for new code.

## Backtrace capture needs nightly, and there are two ways it behaves

The Error trait's `provide()` method is implemented to supply whichever field has a type named `Backtrace`, as a `std::backtrace::Backtrace`. The README is explicit about the cost: using `Backtrace` in errors requires a nightly compiler with Rust 1.73 or newer.

The behaviour splits into two cases, and the difference is subtle enough to be worth reading twice. If a field is both a source and is marked `#[backtrace]`, then `provide()` is forwarded to the source's own `provide` so both layers of the error share a single backtrace. If instead you use `#[from]` on a variant that also contains a `Backtrace` field, the backtrace is captured from inside the generated `From` impl:

```rust
#[derive(Error, Debug)]
pub enum MyError {
    Io {
        #[backtrace]
        source: io::Error,
    },
}
```

Both examples are in the README, and the pair is the practical reason to read that section even if you never write the nightly-only version: the distinction between forwarding an existing capture and creating a new one at conversion time is the kind of thing that is expensive to discover by experiment.

## Transparent variants for opaque public errors

The `error(transparent)` form forwards both the source and the Display method straight through to an underlying error without adding a message. The README gives two reasons to reach for it. The obvious one is an anything-else variant on an enum, where wrapping a third-party error with your own text adds nothing:

```rust
#[derive(Error, Debug)]
pub enum MyError {
    ...

    #[error(transparent)]
    Other(#[from] anyhow::Error),
}
```

The second reason is more interesting and is a versioning technique rather than a convenience. A public error type can be marked transparent over a private representation, which lets you change the internal structure freely across minor versions without breaking the crate's public API:

```rust
// PublicError is public, but opaque and easy to keep compatible.
#[derive(Error, Debug)]
#[error(transparent)]
pub struct PublicError(#[from] ErrorRepr);

impl PublicError {
    // Accessors for anything we do want to expose publicly.
}

// Private and free to change across minor version of the crate.
#[derive(Error, Debug)]
enum ErrorRepr {
    ...
}
```

If you publish a library with an error type, this pattern is the one to consider, because it converts future refactors from breaking changes into internal edits. Accessors on the public wrapper are where you decide what callers actually get to see.

## Repository layout, crate metadata and release cadence

The tree is short and explains the architecture. `src/` holds the facade crate, `impl/` is the proc macro that does the real work, `build.rs` drives the build, and `tests/` covers behaviour including a `tests/no-std` workspace member. The README is the documentation, and it is genuinely complete on the attribute set.

`Cargo.toml` carries the constraints that matter for adoption. The `rust-version` is 1.71, so the crate keeps compatibility with a fairly old compiler. The license is `MIT OR Apache-2.0`, the edition is 2021, and the categories include `rust-patterns` and `no-std`, which matches the default `std` feature being optional. That feature is not incidental: its own comment in the manifest explains that enabling it adds formatting support for `std::path::{Path, PathBuf}` in error messages, so a `PathBuf` field can be interpolated directly instead of writing `.display()` in every message.

The dev-dependencies are as revealing as the code. `anyhow` is there because the README documents them side by side, `trybuild` with the diff feature runs compile-fail tests for error messages, `rustversion` gates tests on compiler version, and `ref-cast` is present for a reason the manifest does not explain. Version 2.0.20, published 2026-08-08, suppresses a redundant field names clippy lint in generated code. Version 2.0.19, published 2026-07-18, updates to syn 3, and 2.0.18, published 2026-01-18, makes the crate compatible with a project-level `needless_lifetimes` set to forbid. Those three releases are what maintenance looks like for a crate whose entire output is generated code: nothing changes for users, and everything changes for the compiler.

## Conclusion

thiserror is a small crate with a narrow job, and it is worth understanding precisely because the narrowness is the design. You write the error type, the crate writes the boilerplate, and because the macro does not appear in your public API, moving between handwritten impls and derived ones is not a breaking change for your users. The attribute set is the real interface: #[error] for the message, #[from] to generate conversion, #[source] to mark the cause, #[backtrace] to share a capture between layers, and error(transparent) to pass both through untouched. Add anyhow when you want one error type and do not care which, and thiserror when your callers need to match on specific failures. Start with the enum example in the README, since the two libraries answer different questions rather than competing.

## FAQ

### What is the difference between `thiserror` and `anyhow` in Rust?

They answer different questions. The README says to use thiserror when you care about designing dedicated error types so callers receive exactly the information you choose, which is typical of library code, and anyhow when you do not care what error type your functions return and just want it to be easy, which is typical of application code. The two compose, and a transparent variant can wrap an anyhow error inside a thiserror enum.

### Does thiserror appear in my crate's public API?

No, and the README makes a point of it. You get the same trait implementations as if you had written them by hand, so switching between handwritten impls and the derive macro is not a breaking change for downstream users. Consumers never need to add the dependency themselves.

### What is the difference between #[from] and #[source]?

A #[from] attribute generates a From impl so a lower level error converts into your variant, and it requires the variant to hold nothing else beyond that error and possibly a backtrace. #[source] marks a field as the underlying cause for the Error trait's source() method without generating a conversion. A #[from] field is always also a #[source], so specifying both is unnecessary.

### What Rust version does thiserror require?

The manifest sets rust-version to 1.71 for the crate itself. Backtrace support is the exception: fields typed as Backtrace are captured through the Error trait's provide method and require a nightly compiler with Rust 1.73 or newer.

## Sources

- [dtolnay/thiserror on GitHub](https://github.com/dtolnay/thiserror)
- [Issues](https://github.com/dtolnay/thiserror/issues)
- [License: Apache-2.0](https://github.com/dtolnay/thiserror/blob/master/LICENSE)
- [README](https://github.com/dtolnay/thiserror/blob/master/README.md)
- [Releases](https://github.com/dtolnay/thiserror/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dtolnay-thiserror
