RustArena

Step 4 of 7

From: teaching ? to convert errors

Box<dyn Error> accepts every error, but it forgets which one it was. For an error type that callers can match on, you want an enum that wraps the lower-level errors:

enum ConfigError {
    MissingEquals(String),       // the bad line
    BadNumber(ParseIntError),    // the parse error, kept inside
}

Now try to use ? on a parse() in a function returning Result<_, ConfigError>:

use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    BadNumber(ParseIntError),
}

fn parse_value(text: &str) -> Result<u32, ConfigError> {
    let n = text.trim().parse::<u32>()?;
    Ok(n)
}

fn main() {
    println!("{:?}", parse_value("12"));
}
error[E0277]: `?` couldn't convert the error to `ConfigError`
  |                         --------------^ the trait `From<ParseIntError>` is not implemented for `ConfigError`
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

The note tells the whole story. Here's what ? really does on an error:

match expression {
    Ok(value) => value,
    Err(e) => return Err(From::from(e)),   // convert, then return
}

From is the standard trait for "make one type out of another". When you call ?, Rust looks for a way to make the function's error type from the error it got. Box<dyn Error> has From for every error type, and also for &str and String messages: that's why those conversions worked in the last step. For your own enum, you write the conversion:

use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    MissingEquals(String),
    BadNumber(ParseIntError),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::MissingEquals(line) => write!(f, "missing '=' in {line:?}"),
            ConfigError::BadNumber(e) => write!(f, "bad number: {e}"),
        }
    }
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::BadNumber(e)
    }
}

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(e) => println!("error: {e}"),
        }
    }
}
timeout = 30
error: bad number: invalid digit found in string
error: missing '=' in "verbose"

The quick alternative: map_err with a variant

For a one-off conversion, map_err works too, and a tuple variant can be passed as if it were a function:

let value: u32 = value.trim().parse().map_err(ConfigError::BadNumber)?;

Use From when the same conversion happens in many places (it's then automatic everywhere), and map_err when it's a single spot or when the same source error should become different variants in different places.

Common mistakes

Your task

A scoreboard reads lines like ferris:120. Implement From<ParseIntError> for ScoreError, then write parse_score: split at the first : (no colon is ScoreError::MissingColon), trim both parts, and parse the points with ? so a bad number becomes ScoreError::BadPoints automatically.

Challenge 2

A product import reads rows like 42;19.99 (an id and a price). Implement two From conversions for ImportError, one from ParseIntError (to BadId) and one from ParseFloatError (to BadPrice), then write parse_row using ? for both parses. A row without ; is ImportError::MissingField. Trim the parts.

Challenge 3

Errors come in layers. A shop's checkout calls pay from a payment module, which has its own PaymentError. Write the shop's error type and use ? across both layers:

Previous: The Error trait and Box<dyn Error> Next: Source chains: errors with causes