RustArena

Step 6 of 6

thiserror or anyhow? Both.

Now the decision you'll make on every project:

thiserror anyhow
Use it in libraries, and modules other code calls applications: main, CLIs, services, scripts, tests
Error type your enum, one variant per failure one anyhow::Error for everything
Callers can match on variants and react report it (and rarely, downcast)
You get Display, Error, From impls ? on anything, context, a good main

The rule of thumb: will the caller handle different failures differently? Then they need variants to match on: thiserror. Will the error just be reported? anyhow.

Most real projects use both: the library part (a lib.rs, or modules like inventory or db) defines thiserror enums, and the application part uses anyhow on top. They fit together: a thiserror enum implements std::error::Error, so ? converts it into anyhow::Error, and context adds the application's story.

mod inventory {
    use std::collections::BTreeMap;
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum InventoryError {
        #[error("unknown item {0:?}")]
        UnknownItem(String),
        #[error("only {available} {item} left, asked for {wanted}")]
        OutOfStock { item: String, available: u32, wanted: u32 },
    }

    #[derive(Default)]
    pub struct Inventory {
        stock: BTreeMap<String, u32>,
    }

    impl Inventory {
        pub fn add(&mut self, item: &str, qty: u32) {
            *self.stock.entry(item.to_string()).or_insert(0) += qty;
        }

        pub fn take(&mut self, item: &str, wanted: u32) -> Result<u32, InventoryError> {
            let available = self
                .stock
                .get_mut(item)
                .ok_or_else(|| InventoryError::UnknownItem(item.to_string()))?;
            if *available < wanted {
                return Err(InventoryError::OutOfStock { item: item.to_string(), available: *available, wanted });
            }
            *available -= wanted;
            Ok(*available)
        }
    }
}

use anyhow::{Context, Result};
use inventory::{Inventory, InventoryError};

fn ship(inv: &mut Inventory, order: &str) -> Result<u32> {
    let (item, qty) = order.split_once(' ').context("expected <item> <qty>")?;
    let qty: u32 = qty.parse().with_context(|| format!("bad quantity {qty:?}"))?;
    let left = inv.take(item, qty).with_context(|| format!("shipping {order:?}"))?;
    Ok(left)
}

fn main() {
    let mut inv = Inventory::default();
    inv.add("mug", 3);
    for order in ["mug 2", "mug 5", "lamp 1", "mug two"] {
        match ship(&mut inv, order) {
            Ok(left) => println!("shipped {order}, {left} left"),
            Err(e) => match e.downcast_ref::<InventoryError>() {
                Some(InventoryError::OutOfStock { .. }) => println!("backorder: {e:#}"),
                _ => println!("error: {e:#}"),
            },
        }
    }
}
shipped mug 2, 1 left
backorder: shipping "mug 5": only 1 mug left, asked for 5
error: shipping "lamp 1": unknown item "lamp"
error: bad quantity "two": invalid digit found in string

Piece by piece:

Panics, once more

Errors are for things that can go wrong in a correct program: bad input, missing files, network failures. Panics are for bugs: an invariant your own code broke. At work: libraries never panic on bad input, expect("reason") documents why something can't fail, and many teams turn on clippy's unwrap_used lint in library code so reviewers see every place that could crash.

Common mistakes

Your task

Finish this inventory tool. First give InventoryError the thiserror derive and messages: UnknownItem prints unknown item "lamp" (Debug-format the name) and OutOfStock prints only 1 mug left, asked for 5. Then write apply, which runs one command line and returns a message:

main is written: it prints backorder: ... when the error is an OutOfStock and error: ... otherwise.

Challenge 2

Map errors to exit codes (the BSD sysexits convention many tools follow). Write fn exit_code(e: &anyhow::Error) -> u8 that downcasts, checking in this order: a ConfigError gives 78, a std::io::Error gives 74, a std::num::ParseIntError gives 65, anything else 1. The errors may have context layers around them.

Challenge 3

Code review: this library function panics on bad input, which a library must never do. Make it return Result<Rgb, ColorError> instead. Define ColorError with thiserror: MissingHash (color must start with #), WrongLength(usize) (expected 6 hex digits, got 4), and BadDigit wrapping a std::num::ParseIntError with #[from] (invalid hex digit). No unwrap, expect or panic! left. (You may assume the input is ASCII, so slicing hex[0..2] is safe.)

Previous: main returns anyhow::Result