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"
impl From<ParseIntError> for ConfigErrorreads "here's how to make aConfigErrorfrom aParseIntError". The one method,from, takes the error and returnsSelf(the type being implemented,ConfigError).- In
parse_line,value.trim().parse()?now compiles: on error,?callsConfigError::from(e), which wraps it inBadNumber. - The original
ParseIntErroris kept inside, not turned into text, so nothing is lost. - A
Fromimplementation also gives you.into()for free:let e: ConfigError = parse_error.into();.
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
- Two variants wrapping the same error type. You can only write one
From<ParseIntError> for ConfigError, so?can't know whether a parse error is a bad port or a bad timeout. Usemap_errto pick the variant at each call site, or keep the context in the variant:BadNumber { key: String, source: ParseIntError }. - Writing
Fromin the wrong direction.impl From<ConfigError> for ParseIntErrorwould convert the other way. The type afterforis the one being created. - Converting the error to a String too early.
BadNumber(String)withe.to_string()loses the original error and its type. Keep the error value; the next step shows why that matters.
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:
ShopError(deriveDebugandPartialEq) with variantsBadQuantity(ParseIntError)andPayment(PaymentError)- a
Fromimplementation for each of those two error types fn checkout(qty_text: &str, unit_price: u32, balance: u32) -> Result<u32, ShopError>: parse the trimmed quantity with?, then callpay(balance, qty * unit_price)?and return the remaining balance
Previous: The Error trait and Box<dyn Error> Next: Source chains: errors with causes