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>:
dyn Errormeans "a value of some type that implementsError; which one is decided at run time". Module 25 covers these trait objects.Box<...>puts that value on the heap, because different error types have different sizes. Module 29 coversBox.
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:
EmptyLine, your own (a unit struct: an error with no data), boxed by hand withBox::new.- A
&strmessage:ok_or("expected ITEM,QTY")makes aResult<_, &str>, and?boxes the message. ParseIntErrorfromparse, boxed by?.
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:
- Applications (a
main, a script, a command-line tool) mostly just report errors:Box<dyn Error>(oranyhow, step 7) is great. - Libraries and modules whose callers need to react to specific failures should return a concrete error enum.
Common mistakes
- Implementing Error without Display:
#[derive(Debug)]
struct EmptyLine;
impl std::error::Error for EmptyLine {}
fn main() {}
error[E0277]: `EmptyLine` doesn't implement `std::fmt::Display`
- Returning your error unboxed.
return Err(EmptyLine);in a function returningBox<dyn Error>is amismatched typeserror:expected `Box<dyn Error>`, found `EmptyLine`. WriteErr(Box::new(EmptyLine))orErr(EmptyLine.into()). Only?boxes automatically. - Comparing
Box<dyn Error>results withassert_eq!. They don't implementPartialEq. In tests, compare the message:assert_eq!(err.to_string(), "line is empty").
Your task
The starter has a TooMany error with Display already written.
- Make it a real error type by implementing
Errorfor it. - Write
parse_ticket_count: trim the input, parse it as au32with?, and if the count is above 10, return a boxedTooMany { 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 each line, use
?on the line itself (line?) and on the parse, then printreading <n>: <temperature>(counting from 1) - at the end print
max: <highest>. If there were no readings, fail instead with the messageno readings:max.ok_or("no readings")?turns theOptioninto an error that?boxes.
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>>:
- no comma: the error message is
expected LAT,LON - either number invalid: pass on the
ParseFloatErrorunchanged (its message isinvalid float literal, orcannot parse float from empty stringfor an empty part) - latitude outside -90 to 90:
latitude <lat> is out of range(build it withformat!and.into())
Trim the parts around the comma.
Previous: Display: messages for people Next: From: teaching ? to convert errors