🎓 A1Learner
🔍

Rust thiserror Patterns

thiserror patterns are the extra derive forms for error enums with extra needs.

Quote named fields in the message

The names in braces are the variant's own field names.

#[error("store header is wrong (expected {expected}, found {found})")]
Header { expected: String, found: String },

Transparent forwards everything

This variant prints the inner error's own message, unchanged.

#[error(transparent)]
Parse(#[from] ParseError),

Wrap a whole smaller enum

A small area's error becomes one variant of the big one. Here it carries its own message on top. It is not transparent.

#[error("store problem")]
Store(#[from] StoreError),

Print a field with Debug

Write {found:?} in the message when the field should print in its Debug form — quotes and all.

#[error("expected a header, found {found:?}")]
BadHeader { found: String },

Mark the cause with #[source]

For an error you build yourself, #[source] names the field that source() returns — no conversion needed.

#[error("could not load the store")]
Load {
    #[source]
    cause: std::io::Error,
},

A second io error? Convert by hand

When one variant already takes io::Error by #[from], build the other variant yourself.

let backup = fs::read_to_string(backup_path)
    .map_err(StoreError::Backup)?;

Example

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

#[derive(Debug, Error)]
enum StoreError {
    #[error("could not read the store file")]
    Read(#[from] std::io::Error),
    #[error("store header is wrong (expected {expected}, found {found})")]
    Header { expected: String, found: String },
    #[error(transparent)]
    Parse(#[from] ParseError),
}

#[derive(Debug, Error)]
enum ParseError {
    #[error("line {0} is not a number")]
    BadLine(usize),
}

fn load_total(path: &str) -> Result<i32, StoreError> {
    let text = fs::read_to_string(path)?;
    let mut lines = text.lines();
    let header = lines.next().unwrap_or("");
    if header != "STORE1" {
        return Err(StoreError::Header {
            expected: String::from("STORE1"),
            found: header.to_string(),
        });
    }
    let mut total = 0;
    for (i, line) in lines.enumerate() {
        let n: i32 = line.trim().parse().map_err(|_| ParseError::BadLine(i + 2))?;
        total += n;
    }
    Ok(total)
}

fn main() {
    fs::write("store.txt", "STORE1\n10\n20\n").unwrap();
    println!("total: {:?}", load_total("store.txt"));
    fs::write("store.txt", "SHOP\n10\n").unwrap();
    println!("header: {}", load_total("store.txt").unwrap_err());
    fs::write("store.txt", "STORE1\n10\nxyz\n").unwrap();
    println!("line: {}", load_total("store.txt").unwrap_err());
}

Key points

  • Only one #[from] per inner type is allowed in one enum.
  • Two variants cannot both convert from io::Error.
  • Convert the second by hand with map_err.

In this example, StoreError combines two smaller styles. Header is a named-field variant. Its message quotes {expected} and {found} straight from the fields. That is how the second file earns its message. The message reads store header is wrong (expected STORE1, found SHOP). Parse is a whole second error enum, ParseError. It sits wrapped in one variant marked transparent. When line 3 fails to parse, the printed message is ParseError's own. It reads line 3 is not a number. StoreError adds nothing. The first file loads cleanly and totals 30. Three failures, three shapes, one enum.

Note

thiserror never appears in your public API.

The derive writes exactly the impls you would write by hand, so swapping styles later is not a breaking change.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does #[error(transparent)] on a variant do?