RustArena

Step 2 of 6

Wrapping causes: #[from], #[source], transparent

Real errors have causes. Your config loader fails because a number didn't parse; your importer fails because the file wasn't there. Throwing the cause away makes debugging miserable, so std::error::Error has a method for it: source() returns the error that caused this one (or None). Following source() again and again walks the error chain.

thiserror sets up source() from attributes on fields:

#[source], with extra information

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

#[derive(Debug, Error)]
enum ConfigError {
    #[error("line {line}: expected key=value")]
    Syntax { line: usize },
    #[error("line {line}: invalid number for {key}")]
    BadNumber {
        line: usize,
        key: String,
        #[source]
        source: ParseIntError,
    },
}

fn parse_config(text: &str) -> Result<Vec<(String, u32)>, ConfigError> {
    let mut out = Vec::new();
    for (i, raw) in text.lines().enumerate() {
        let line = i + 1;
        let Some((key, value)) = raw.split_once('=') else {
            return Err(ConfigError::Syntax { line });
        };
        let key = key.trim().to_string();
        let n = value
            .trim()
            .parse()
            .map_err(|source| ConfigError::BadNumber { line, key: key.clone(), source })?;
        out.push((key, n));
    }
    Ok(out)
}

fn print_chain(e: &dyn Error) {
    println!("error: {e}");
    let mut cause = e.source();
    while let Some(c) = cause {
        println!("  caused by: {c}");
        cause = c.source();
    }
}

fn main() {
    println!("{:?}", parse_config("workers = 4\nport = 8080"));
    if let Err(e) = parse_config("workers = 4\nport = eighty") {
        print_chain(&e);
    }
    if let Err(e) = parse_config("workers") {
        print_chain(&e);
    }
}
Ok([("workers", 4), ("port", 8080)])
error: line 2: invalid number for port
  caused by: invalid digit found in string
error: line 1: expected key=value

The variant carries extra facts (the line and key), so there's no automatic conversion: map_err builds the variant and ? returns it. print_chain takes &dyn Error, so it works for any error type.

#[from]: automatic conversion with ?

When a variant only wraps another error, #[from] saves the map_err:

use thiserror::Error;

#[derive(Debug, Error)]
enum ImportError {
    #[error("could not read the export file")]
    Io(#[from] std::io::Error),
    #[error("export file is not valid JSON")]
    Json(#[from] serde_json::Error),
}

fn import(path: &str) -> Result<Vec<u32>, ImportError> {
    let text = std::fs::read_to_string(path)?; // io::Error becomes ImportError::Io
    let ids = serde_json::from_str(&text)?; // serde_json::Error becomes ImportError::Json
    Ok(ids)
}

fn main() {
    match import("export.json") {
        Ok(ids) => println!("{} ids", ids.len()),
        Err(e) => {
            println!("{e}");
            if let Some(cause) = std::error::Error::source(&e) {
                println!("because: {cause}");
            }
        }
    }
}
could not read the export file
because: No such file or directory (os error 2)

Don't say it twice

Notice the messages above describe only their own layer ("could not read the export file"), not the cause. If you write #[error("bad number: {0}")] and mark the field as the source, every tool that prints the chain shows the cause twice: bad number: invalid digit found in string: invalid digit found in string. Pick one: either the message includes the cause and the field isn't a source, or (the common choice) the cause is the source and the message leaves it out.

#[error(transparent)]

Sometimes a variant adds nothing: it just passes another error through. #[error(transparent)] forwards both Display and source() to the inner error:

#[derive(Debug, Error)]
enum StoreError {
    #[error("item {0} not found")]
    NotFound(u32),
    #[error(transparent)]
    Io(#[from] std::io::Error),
}

Common mistakes

Two variants can't both have #[from] for the same type: ? wouldn't know which one to build.

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

#[derive(Debug, Error)]
enum ScoreError {
    #[error("bad score")]
    BadScore(#[from] ParseIntError),
    #[error("bad round")]
    BadRound(#[from] ParseIntError),
}

fn main() {}
error[E0119]: conflicting implementations of trait `From<ParseIntError>` for type `ScoreError`

Use #[source] plus map_err for at least one of them. Also, #[from] only works on a variant whose other fields are empty; adding line: usize next to it gives deriving From requires no fields other than source and backtrace. Again, switch to #[source].

Your task

parse_quantity is already written and uses ? on a ParseIntError. Define QuantityError so it compiles and passes: a variant NotANumber that wraps the ParseIntError with #[from] (message not a whole number), and TooLarge(u32) with message quantity 250 is over the maximum of 100.

Challenge 2

Add the line number. Write fn parse_scores(text: &str) -> Result<Vec<u32>, ScoreError> that parses one score per line. A bad line gives ScoreError::BadLine { line, source } (line counts from 1), with message line 3 is not a score and the ParseIntError as its source. Since the variant has an extra field, use map_err, not #[from].

Challenge 3

Write fn error_chain(e: &dyn Error) -> Vec<String> that returns the message of e followed by the message of each cause, outermost first. The error types in the starter code are ready to use; the tests build nested errors with them.

Previous: Library errors with thiserror Next: anyhow: one error type for applications