🎓 A1Learner
🔍

Rust thiserror

thiserror is a crate that writes error boilerplate for a custom error enum. A derive on the enum generates the Display and Error impls.

The long way: Display and Error written by hand

Without thiserror, one variant needs a Display impl and an Error impl written out in full:

use std::fmt;
#[derive(Debug)]
enum LoadError {
    Range(i32),
}
impl fmt::Display for LoadError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            LoadError::Range(n) => write!(f, "number {n} is out of range"),
        }
    }
}
impl std::error::Error for LoadError {}
fn main() {
    println!("{}", LoadError::Range(500));
}

Add the crate, then derive

Put thiserror = "2" under [dependencies] in Cargo.toml. The derive then writes your Error and Display impls for you.

use thiserror::Error;

#[derive(Debug, Error)]
enum LoadError {
    #[error("number {0} is out of range")]
    Range(i32),
}

The #[error] text is the Display

{0} in the text means the variant's first field. Printing the error fills it in.

println!("{}", LoadError::Range(500));

#[from] converts through ?

Mark a field #[from] and ? turns that error into your variant by itself. The field also becomes the error's source().

#[error("bad number in file: {0}")]
Parse(#[from] std::num::ParseIntError),

Callers can still match

Your error stays a plain enum, so a caller can match a variant and react to that kind of failure.

let err = LoadError::Range(500);
match err {
    LoadError::Range(n) => println!("caller matched Range({n})"),
    _ => println!("other"),
}

Example

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

#[derive(Debug, Error)]
enum LoadError {
    #[error("could not read file: {0}")]
    Read(#[from] std::io::Error),
    #[error("bad number in file: {0}")]
    Parse(#[from] std::num::ParseIntError),
    #[error("number {0} is out of range")]
    Range(i32),
}

fn load_number(path: &str) -> Result<i32, LoadError> {
    let text = fs::read_to_string(path)?;
    let n: i32 = text.trim().parse()?;
    if !(1..=100).contains(&n) {
        return Err(LoadError::Range(n));
    }
    Ok(n)
}

fn main() {
    fs::write("score.txt", "42").unwrap();
    println!("good: {:?}", load_number("score.txt"));
    fs::write("score.txt", "abc").unwrap();
    println!("bad text: {}", load_number("score.txt").unwrap_err());
    fs::write("score.txt", "500").unwrap();
    println!("too big: {}", load_number("score.txt").unwrap_err());
}

Key points

  • The derive writes the std::error::Error impl and the Display impl.
  • Those are the two impls Rust Custom Errors wrote by hand.

In this example, LoadError is an enum with three variants, and thiserror does the rest. The Read and Parse variants carry #[from], so when fs::read_to_string or parse fails inside load_number, the ? converts the failure into the right variant by itself — no map_err, no manual From impl. main writes three test files in turn. "42" loads fine: Ok(42). "abc" fails the parse. unwrap_err prints the Display sentence. The #[error] text built that sentence. It reads bad number in file: invalid digit found in string. 500 parses but breaks the range rule, and Range's own message prints: number 500 is out of range.

Note

thiserror only removes typing.

thiserror suits error types that other code will match on — library-style errors.

A later lesson in this section meets anyhow, the crate for the app side.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does #[from] on a variant field give you?