The ? operator lets a successful computation continue and propagates an
early-exit value when it cannot. For everyday Result code, think of it
as a short match with an error conversion.
Start with Result
For let value = operation()?;, the useful mental model is:
let value = match operation() {
Ok(value) => value,
Err(error) => return Err(From::from(error)),
};
The Ok case produces the inner value. The Err case returns from the
enclosing function, converting the error into its return type. This is
ordinary error propagation, not a panic or exception.
For example, a parser returning ParseIntError can be used inside a
function returning MyError when MyError: From<ParseIntError>:
use std::num::ParseIntError;
#[derive(Debug)]
enum MyError {
InvalidNumber(ParseIntError),
}
impl From<ParseIntError> for MyError {
fn from(error: ParseIntError) -> Self {
Self::InvalidNumber(error)
}
}
fn parse_number(text: &str) -> Result<i32, MyError> {
let number = text.parse::<i32>()?;
Ok(number)
}
fn main() {
assert_eq!(parse_number("42").unwrap(), 42);
match parse_number("bad") {
Err(MyError::InvalidNumber(error)) => {
assert_eq!(error.kind(), &std::num::IntErrorKind::InvalidDigit);
}
Ok(_) => panic!("expected a parse failure"),
}
}
Without the From implementation, this explicit parse produces a missing
conversion error, typically E0277. When both error types already match,
the standard identity implementation From<T> for T is sufficient.
Conversion is useful, but it is not the only purpose of ?: even without
conversion, it removes repeated early-return matches.
Option needs an explicit bridge
With Option, Some(value)? produces the value and None? returns
None. It does not automatically turn an error into an absent value or
invent an error for you.
fn optional_number(text: &str) -> Option<i32> {
let number = text.parse::<i32>().ok()?;
Some(number)
}
fn required_first(values: &[i32]) -> Result<i32, &'static str> {
let first = values.first().ok_or("empty input")?;
Ok(*first)
}
fn main() {
assert_eq!(optional_number("7"), Some(7));
assert_eq!(optional_number("bad"), None);
assert_eq!(required_first(&[9]), Ok(9));
assert_eq!(required_first(&[]), Err("empty input"));
}
.ok() deliberately discards the parse error. .ok_or(...) supplies an
error for an absent value; .ok_or_else(...) builds it only when needed.
Choose the bridge according to whether the caller needs the failure detail.
Check the return boundary
An ordinary block does not create a new boundary for ?. A closure does,
and an async block propagates into that block's future output rather than
returning from the surrounding synchronous function.
This complete example uses one ? inside a closure and another after
collection:
fn lookup(id: u32) -> Result<&'static str, &'static str> {
match id {
1 => Ok("Ada"),
2 => Ok("Grace"),
_ => Err("unknown id"),
}
}
fn names(ids: &[u32]) -> Result<Vec<String>, &'static str> {
let names = ids
.iter()
.map(|id| -> Result<String, &'static str> {
Ok(lookup(*id)?.to_owned())
})
.collect::<Result<Vec<_>, _>>()?;
Ok(names)
}
fn main() {
assert_eq!(names(&[2, 1]).unwrap(), ["Grace", "Ada"]);
assert_eq!(names(&[1, 99, 2]), Err("unknown id"));
assert!(names(&[]).unwrap().is_empty());
}
A failed lookup returns from that closure invocation. Collecting into
Result<Vec<_>, _> then stops at the first error, and the outer ?
propagates it from names. Collecting into Vec<Result<_, _>> instead
would retain a result for each input and continue iteration.
The trait model behind the operator
The following is conceptual pseudocode, not a stable Rust snippet to copy:
match Try::branch(expression) {
ControlFlow::Continue(value) => value,
ControlFlow::Break(residual) => {
return FromResidual::from_residual(residual);
}
}
Try::branch separates continuation from early exit.
FromResidual builds the enclosing return value from the early-exit
part, called a residual.
For Result<T, E>, the residual is Result<Infallible, E>.
Infallible has no values, so a valid residual cannot contain an
Ok value. For Option<T>, the residual is Option<Infallible>,
whose only value is None.
These distinct residual types let the trait implementations distinguish
the source of an early exit. Sharing an error payload does not by itself
make two containers interchangeable. The provided Result conversion uses
From<E>; Option has no error payload to convert.
Standard types such as ControlFlow also support ?. Using the operator
is stable, but implementing the underlying Try and FromResidual
traits for your own types requires unstable features on the stable
toolchain used here. The trait model explains compatibility; it does not
promise an exact branch count or machine-code sequence.
Conversion is different from type equality
A bound such as T: FromStr<Err = MyError> requires T's parser error to
be exactly MyError. A From implementation does not make two types equal.
To accept convertible parser errors, use separate bounds and perform the
conversion. With the MyError type and From implementation from the first
example, this function accepts i32:
fn parse_converting<T>(text: &str) -> Result<T, MyError>
where
T: std::str::FromStr,
MyError: From<T::Err>,
{
Ok(text.parse::<T>()?)
}
By contrast, a helper requiring FromStr<Err = MyError> rejects i32,
whose parser error is ParseIntError. The From implementation cannot
satisfy that equality constraint.
When diagnosing ?, check the operand type, the return boundary and its
output type. Then check whether the diagnostic requires a conversion or
an exact associated-type equality. Inferred types can affect which error
appears first.