? is the piece of Rust that most reliably gets described as magic, which is a
shame, because it is about four lines of ordinary code and knowing those four
lines explains every error message it will ever produce.
The first approximation
For a Result, this:
let n = might_fail()?;
means this:
let n = match might_fail() {
Ok(v) => v,
Err(e) => return Err(From::from(e)),
};
Two things are happening, and beginners usually notice only the first.
-
The success case unwraps.
nis the value inside theOk. -
The failure case returns — with a conversion.
From::from(e)turns the error you have into the error your function returns.
That second half is the whole reason ? is worth having. Without it you would
be writing map_err at every call site to reconcile error types. With it, you
write one From impl per error pair and the plumbing disappears.
💡The desugaring says return Err(From::from(e)). If your function returns Result<T, MyError> and e is a ParseIntError, which From impl gets used — and what happens if there isn't one?
click to reveal
The impl used is impl From<ParseIntError> for MyError, chosen by type
inference from the return type of the enclosing function. If it does not
exist, the code does not compile, and the diagnostic is 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 exactly which impl to go and write.
There is one impl you always get for free: impl<T> From<T> for T, the
identity. That is why ? works with no effort at all when the error types
already match — the conversion is still happening, it is just a no-op.
The real desugaring
The approximation above is what you should carry around day to day. The literal one, which is what the compiler actually does, is:
match Try::branch(expr) {
ControlFlow::Continue(v) => v,
ControlFlow::Break(r) => return FromResidual::from_residual(r),
}
Two traits. Try::branch splits a value into “carry on with this” or “stop
with this”; FromResidual::from_residual builds the enclosing function’s
return value out of the stopping part.
The word residual is the one worth pausing on. You might expect Try to say
“my error type is E“. It does not. For Result<T, E>, the residual type is
Result<Infallible, E>
— a Result that cannot be Ok, because Infallible is a type with no
values at all. So the thing that flows out of a ? is not a bare error; it is
“a Result known to be in its Err arm”.
That indirection is not decoration. It is what keeps the type of the operand
and the type of the enclosing function connected, so that ? on a
ControlFlow cannot silently be used in a Result-returning function even
though both go through the same two traits. Different residual, no impl, no
compile.
💡Why go to the trouble of an uninhabited type? Wouldn't Residual = E have been simpler?
click to reveal
It would have been simpler and it would have been wrong, for a reason that
shows up the moment two Try types share an error type.
FromResidual is what decides which return types a given ? is legal in. If
the residual for Result<T, E> were just E, then any type whose residual was
also E would become interchangeable with it — the impl could not tell them
apart. Wrapping it as Result<Infallible, E> stamps the source into the
residual type, so impl<T, E, F: From<E>> FromResidual<Result<Infallible, E>> for Result<T, F> accepts residuals from Result and nothing else.
Option‘s residual is Option<Infallible> for the same reason. And because
that type carries no payload, there is nothing to convert — which is precisely
why ? on an Option does no From conversion at all.
The Infallible half is uninhabited so the compiler knows the Ok/Some arm
is unreachable and optimises the whole thing away. At runtime ? is a branch,
nothing more.
Where ? works
The Try implementations in std today:
-
Result<T, E> -
Option<T> -
ControlFlow<B, C> -
Poll<Result<T, E>>andPoll<Option<Result<T, E>>>— the async ones, so?works inside apollfunction
You cannot add to that list. Try is still unstable on Rust 1.95
(try_trait_v2, tracking issue #84277), so you cannot implement it for your
own types. If you have ever wondered why nobody’s crate lets you ? on their
custom wrapper — that is why.
Two things it is not
? does not return from a block. It returns from the enclosing function
or closure. This matters constantly:
let names: Vec<String> = ids
.iter()
.map(|id| lookup(*id)?.name.clone()) // returns from the CLOSURE
.collect();
The ? there exits the closure, not the surrounding function, so the closure’s
return type has to be a Result — and then collect has to deal with an
iterator of results. If you wanted “abandon the whole function”, the ? has to
be outside the closure.
This is also why try { … } blocks are a separate feature: a block that ?
can return from is a genuinely different thing, and it is still experimental:
error[E0658]: `try` expression is experimental
? does not work in a const fn. Verified on 1.95:
error[E0658]: `?` is not allowed on `Option<i32>` in constant functions
= note: see issue #143874
The traits it desugars to are not const, so there is nowhere for it to hang yet.
💡You are writing fn first_word_len(s: &str) -> Option<usize> and inside it you call s.parse::<i32>(), which returns a Result. Can you ? that?
click to reveal
No — and the error says so directly:
error[E0277]: the `?` operator can only be used on `Option`s, not `Result`s,
in a function that returns `Option`
The desugaring explains why. ? on a Result produces a residual of type
Result<Infallible, ParseIntError>, and Option<usize> has no
FromResidual<Result<Infallible, _>> impl. There is no automatic bridge,
deliberately: turning a Result into an Option throws the error away, and
Rust will not do that behind your back.
The fix is to throw it away explicitly — s.parse::<i32>().ok()? — or, more
often, to notice that your function should have been returning a Result in
the first place.
Going the other way (a ? on an Option inside a Result function) has the
mirror-image error, and its fix is .ok_or(…)? or .ok_or_else(…)?, because
there you have to invent an error rather than discard one.
The error message that does not mention ? at all
This is the one that gets people stuck, so it is worth recognising on sight:
error[E0271]: type mismatch resolving `<i32 as FromStr>::Err == MyErr`
|
| let n: i32 = s.parse()?;
| ^^^^^ expected `MyErr`, found `ParseIntError`
Same cause as the friendly E0277 — a missing From impl — but phrased in terms
of an associated type, because parse‘s error type is
<i32 as FromStr>::Err rather than a concrete name. Nothing in the message
says “question mark” or “From”.
Translate it mentally as: “? needed to convert ParseIntError into MyErr
and could not.” Then go and write
impl From<ParseIntError> for MyErr { … }
and it goes away.
What to take from this
-
?unwraps on success and returns with aFromconversion on failure. - The conversion is the point. It is why error types compose across layers.
-
On
Optionthere is no conversion — there is nothing to convert. - It exits the enclosing function or closure, never merely a block.
-
The
Trymachinery is public but unstable, so this is a mechanism you read rather than extend.
Everything about ? that looks like a special case turns out to be one of
those five facts wearing a different error code.