RustArena

Step 3 of 7

The Error trait and Box<dyn Error>

The standard library has a trait that marks a type as "an error": std::error::Error. Implementing it is one line, because all of its methods have default versions:

impl std::error::Error for WithdrawError {}

There's one requirement: the type must already implement Debug and Display. That's the contract of an error in Rust: it can be shown to developers ({:?}) and to users ({}).

Why bother? Because code that works with any error, from the standard library, from crates like anyhow, and from your own programs, works through this trait. ParseIntError, ParseFloatError and std::io::Error all implement it.

Box: "some error"

A function that can fail in several different ways, with different error types, needs one return type that fits them all. The standard answer is Box<dyn Error>:

Together: Result<T, Box<dyn Error>> means "a T, or any kind of error". And ? converts any error type that implements Error into a Box<dyn Error> for you. Even a plain &str or String message converts.

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct EmptyLine;

impl fmt::Display for EmptyLine {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "line is empty")
    }
}

impl Error for EmptyLine {}

fn parse_order(line: &str) -> Result<(String, u32), Box<dyn Error>> {
    let line = line.trim();
    if line.is_empty() {
        return Err(Box::new(EmptyLine));
    }
    let (item, qty) = line.split_once(',').ok_or("expected ITEM,QTY")?;
    let qty: u32 = qty.trim().parse()?;
    Ok((item.to_string(), qty))
}

fn main() {
    for line in ["apples, 3", "pears,many", "", "plums"] {
        match parse_order(line) {
            Ok((item, qty)) => println!("{qty} x {item}"),
            Err(e) => println!("error: {e}"),
        }
    }
}
3 x apples
error: invalid digit found in string
error: line is empty
error: expected ITEM,QTY

Three different error types flow through one return type:

The caller prints each with {}; the box forwards to the real error's Display.

main returning Box

This fixes the problem from the last module: read_line fails with io::Error, parse with ParseIntError, and a main returning Result<(), Box<dyn Error>> accepts both:

use std::error::Error;
use std::io::BufRead;

fn main() -> Result<(), Box<dyn Error>> {
    let mut total: i64 = 0;
    for line in std::io::stdin().lock().lines() {
        let line = line?;
        let amount: i64 = line.trim().parse()?;
        total += amount;
    }
    println!("total: {total}");
    Ok(())
}
120
-45
30
total: 105

fn main() -> Result<(), Box<dyn Error>> is the standard signature for small programs, scripts and examples.

The trade-off

A Box<dyn Error> has forgotten its concrete type. The caller can print it, but can't match on your variants (getting the type back needs downcasting, module 25). So:

Common mistakes

#[derive(Debug)]
struct EmptyLine;

impl std::error::Error for EmptyLine {}

fn main() {}
error[E0277]: `EmptyLine` doesn't implement `std::fmt::Display`

Your task

The starter has a TooMany error with Display already written.

  1. Make it a real error type by implementing Error for it.
  2. Write parse_ticket_count: trim the input, parse it as a u32 with ?, and if the count is above 10, return a boxed TooMany { requested: <count>, max: 10 }. Otherwise return the count.

Challenge 2

A thermostat log has one temperature (f64) per line. Make main return Result<(), Box<dyn Error>> and:

For input 21.5, 19, 23.25 the output is:

reading 1: 21.5
reading 2: 19
reading 3: 23.25
max: 23.25

A line that isn't a number, or empty input, ends the program with exit code 1.

Challenge 3

A map app reads coordinates as LAT,LON, like 52.52,13.40. Write fn parse_coordinates(text: &str) -> Result<(f64, f64), Box<dyn Error>>:

Trim the parts around the comma.

Previous: Display: messages for people Next: From: teaching ? to convert errors