RustArena

Step 1 of 6

Library errors with thiserror

In the custom errors module you built error enums by hand: the enum, an impl Display with a match giving each variant a message, and an empty impl std::error::Error. That design is exactly right. It's also a lot of typing, and the boilerplate grows with every variant. thiserror is a derive macro that writes those impls for you:

use thiserror::Error;

#[derive(Debug, Error)]
enum WithdrawError {
    #[error("amount must be positive")]
    ZeroAmount,
    #[error("insufficient funds: need {needed}, have {available}")]
    InsufficientFunds { needed: u64, available: u64 },
    #[error("account {0} is frozen")]
    Frozen(String),
}

fn withdraw(balance: u64, amount: u64) -> Result<u64, WithdrawError> {
    if amount == 0 {
        return Err(WithdrawError::ZeroAmount);
    }
    if amount > balance {
        return Err(WithdrawError::InsufficientFunds { needed: amount, available: balance });
    }
    Ok(balance - amount)
}

fn main() {
    println!("{:?}", withdraw(100, 30));
    for amount in [0, 500] {
        match withdraw(100, amount) {
            Ok(left) => println!("left: {left}"),
            Err(e) => println!("error: {e}"),
        }
    }
    println!("{}", WithdrawError::Frozen(String::from("ACC-7")));
}
Ok(70)
error: amount must be positive
error: insufficient funds: need 500, have 100
account ACC-7 is frozen

Piece by piece:

A struct works too, for an operation that can fail in only one way:

use thiserror::Error;

#[derive(Debug, Error)]
#[error("invalid color {input:?}: expected #rrggbb")]
struct ParseColorError {
    input: String,
}

fn main() {
    let e = ParseColorError { input: String::from("teal") };
    println!("{e}");
}
invalid color "teal": expected #rrggbb

Writing good messages

The Rust API guidelines say error messages are lowercase, without a trailing period, and short. The reason: messages get chained into sentences like loading config: line 3: invalid number, and capitals and periods in the middle look wrong. Say what went wrong and include the values that help (the amount, the account, the input).

Common mistakes

Every variant needs a message. Forget one and thiserror tells you at compile time:

use thiserror::Error;

#[derive(Debug, Error)]
enum LoadError {
    #[error("file is empty")]
    Empty,
    TooBig(usize),
}

fn main() {
    println!("{}", LoadError::Empty);
}
error: missing #[error("...")] display attribute

Your task

Complete TransferError (derive Error, add a message to each variant) and write check_transfer, which returns the balance left after the transfer. Check in this order, with these exact messages:

Challenge 2

Fill in the blanks: derive thiserror's trait on the struct, and write the message so the program prints invalid size "XXL": expected S, M or L (use Debug formatting of the input field to get the quotes).

Challenge 3

Write an error enum from scratch. TemperatureError has two variants: NotANumber(String) with message "abc" is not a number (Debug-format the text) and BelowAbsoluteZero(f64) with message -300.0 C is below absolute zero (one decimal). Then write fn parse_celsius(s: &str) -> Result<f64, TemperatureError>: trim s, parse it as f64, and reject anything below -273.15.

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