Skip to content

← Errors Are Values step 15 of 24

Medium Primitives

From impls: the machinery behind `?`

Parse a two-field record with bare ? on both parses — no map_err anywhere.

pub enum AppError { Int(ParseIntError), Float(ParseFloatError), Empty }

pub fn parse_record(line: String) -> Result<(i32, f64), AppError>

"42,3.5" gives Ok((42, 3.5)). Each field is trimmed before parsing, so " 7 , -2.25 " works. A line that trims to nothing is AppError::Empty. A line with no comma has an empty value field, which is a float parse error.

Two contracts are asserted:

  • Display"count field is not an integer", "value field is not a number", "record is empty".
  • source() — for the two wrapping variants it must return the inner error, so that e.source().unwrap().to_string() is std’s own "invalid digit found in string", "invalid float literal", "number too large to fit in target type". For Empty it is None.

This is the keystone item

Everything before this taught you to handle errors. This one makes them compose.

The reason ? looks like magic is that its error path applies From::from:

let count: i32 = left.trim().parse()?;

parse::<i32>() produces Result<i32, ParseIntError>. This function returns Result<_, AppError>. Those are different types, and the code compiles anyway, because ? desugars to (roughly):

match left.trim().parse::<i32>() {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
}

So: write impl From<TheirError> for MyError, and ? starts converting for free at every call site. That is the entire mechanism. It is also exactly what #[from] generates in thiserror. Once these two impls exist, the body of parse_record has no error plumbing in it at all — the happy path reads like a script.

If you get it wrong you will meet E0277 with this note attached:

= note: the question mark operation (`?`) implicitly performs a conversion on
        the error value using the `From` trait

which is rustc telling you, in as many words, exactly which impl is missing.

The trap in the starter, and why it matters

The obvious labour-saving idea is one blanket impl instead of two:

impl<E: Error> From<E> for AppError { … }
error[E0119]: conflicting implementations of trait `From<AppError>`
              for type `AppError`
   |
   = note: conflicting implementation in crate `core`:
           - impl<T> From<T> for T;

You cannot write it. core already contains impl<T> From<T> for T — the identity conversion, which every type gets — and your blanket impl would also cover E = AppError, so the two overlap. There is no way around it: no specialisation, no negative bounds.

This is precisely why Box<dyn Error> and anyhow exist. They are the answer to “I want ? to accept any error without writing an impl per type”, and the answer is type erasure rather than a blanket From. (Box<dyn Error> gets away with a blanket impl because it is a different type from the errors it swallows.) You will meet it two items from now — but the reason it exists is this error message.

Implementing source()

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Int(e) => Some(e),
            AppError::Float(e) => Some(e),
            AppError::Empty => None,
        }
    }
}

Note the return type: Option<&(dyn Error + 'static)>. A borrowed trait object, with a 'static bound on the erased type, not on the reference — which is why an error that borrows from a buffer cannot be a source.

And note what Display does not do. AppError::Int‘s message is “count field is not an integer” and stops there. It does not say “…: invalid digit found in string”. The cause is reachable through source(); duplicating it in Display means every caller that walks the chain prints it twice.

From or map_err?

Both convert an error. The rule that decides:

From when the conversion is context-free. map_err when you need to attach something only the call site knows.

ParseIntErrorAppError::Int needs no context: it is the same information, relabelled. But “which config key was being read” is known only where the read happens, so that is a map_err — or, better, the extension trait you will build in item 6.19.

Notes

  • from_over_into (default-on) rejects impl Into<AppError> for X. Always write the From impl: Into is blanket-implemented from From, so you get both, and only one direction works the other way round.
  • fallible_impl_from flags a From impl containing panic! or unwrap. A From conversion is supposed to be infallible by definition — if yours can fail, you wanted TryFrom.
  • useless_conversion fires on .into() where the types already match, which happens a lot once From impls exist and people start sprinkling .into() defensively.
  • line.split_once(',') returns Option<(&str, &str)>; .unwrap_or((line, "")) handles the no-comma case without a special branch, and lets the empty value field fail naturally as a float.

Remember the grade is compile + tests + clippy -D warnings.