RustArena

Step 7 of 7

A first look at thiserror and anyhow

Writing Display, Error, source and From by hand is a lot of repetitive code. Two crates (Rust libraries) remove almost all of it, and you'll find them in most Rust codebases at work. Add them to a project's Cargo.toml:

[dependencies]
thiserror = "2"
anyhow = "1"

thiserror: error enums without the boilerplate

thiserror provides a derive macro, like #[derive(Debug)], that writes the Display, Error and From implementations for you:

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

#[derive(Debug, Error)]
enum ConfigError {
    #[error("missing '=' in line {0:?}")]
    MissingEquals(String),
    #[error("invalid number")]
    BadNumber(#[from] ParseIntError),
}

fn parse_line(line: &str) -> Result<(String, u32), ConfigError> {
    let (key, value) = line
        .split_once('=')
        .ok_or_else(|| ConfigError::MissingEquals(line.to_string()))?;
    let value: u32 = value.trim().parse()?;
    Ok((key.trim().to_string(), value))
}

fn main() {
    for line in ["timeout = 30", "retries = lots", "verbose"] {
        match parse_line(line) {
            Ok((key, value)) => println!("{key} = {value}"),
            Err(ConfigError::BadNumber(cause)) => println!("invalid number ({cause})"),
            Err(e) => println!("error: {e}"),
        }
    }
}
timeout = 30
invalid number (invalid digit found in string)
error: missing '=' in line "verbose"

It's still an ordinary enum: callers match on it exactly as before. That's why thiserror is the usual choice for libraries and for any module whose callers need to tell errors apart.

anyhow: one error type for applications

In application code you often don't match on errors at all: you add context and report them. anyhow gives you one type, anyhow::Error, that holds any error (like Box<dyn Error>, but with more features), and the alias anyhow::Result<T> for Result<T, anyhow::Error>.

use anyhow::{bail, Context, Result};

fn parse_port(text: &str) -> Result<u16> {
    let port: u16 = text
        .trim()
        .parse()
        .with_context(|| format!("invalid port {text:?}"))?;
    if port < 1024 {
        bail!("port {port} is reserved");
    }
    Ok(port)
}

fn main() {
    for text in ["8080", "80", "http"] {
        match parse_port(text) {
            Ok(port) => println!("listening on {port}"),
            Err(e) => println!("error: {e:#}"),
        }
    }
}
listening on 8080
error: port 80 is reserved
error: invalid port "http": invalid digit found in string

Which one?

Module 39 goes deep into both crates (ensure!, error chains in main, mixing them, designing error types for a real service).

Common mistakes

Your task

Rewrite TicketError with thiserror. Add an #[error(...)] attribute to each variant so that:

Challenge 2

Write ScoreError with thiserror (derive Debug and Error):

Then write fn parse_score(line: &str) -> Result<(String, u32), ScoreError> for lines like ferris:120 (trim both parts), using ? for the parse.

Challenge 3

Use anyhow. Write fn load_limit(text: &str) -> anyhow::Result<u32> that reads a rate limit:

Previous: panic or Result? The guidelines