We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Errors Are Values step 15 of 24
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 thate.source().unwrap().to_string()is std’s own"invalid digit found in string","invalid float literal","number too large to fit in target type". ForEmptyit isNone.
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:
Fromwhen the conversion is context-free.map_errwhen you need to attach something only the call site knows.
ParseIntError → AppError::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) rejectsimpl Into<AppError> for X. Always write theFromimpl:Intois blanket-implemented fromFrom, so you get both, and only one direction works the other way round. -
fallible_impl_fromflags aFromimpl containingpanic!orunwrap. AFromconversion is supposed to be infallible by definition — if yours can fail, you wantedTryFrom. -
useless_conversionfires on.into()where the types already match, which happens a lot onceFromimpls exist and people start sprinkling.into()defensively. -
line.split_once(',')returnsOption<(&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.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.