The question mark through layers
An error that happens at the bottom of a call chain has to reach the caller that can handle it, and every layer in between has a decision to make about it. The question mark is the operator for that trip: it returns the Err from the current function, and the From implementation converts the error type of one layer into the next.
This lesson puts two layers on top of each other, then shows the boxed trait object that can hold any error type at the top, the conversion between two concrete types, and a main that returns the result. The last section is the compiler asking for the conversion between two types that have none.
The question mark through layers
The inner function parses and the outer one calls it, and both use the question mark. Each function returns the error type of its own signature, so the conversion happens at the boundary where the types meet.
The program calls the outer function with good and bad input. The good call doubles twenty-one, the bad one returns the message the inner layer produced.
fn parse(text: &str) -> Result<i32, String> {
let value: i32 = text.parse().map_err(|_| String::from("not a number"))?;
Ok(value)
}
fn double(text: &str) -> Result<i32, String> {
let value = parse(text)?;
Ok(value * 2)
}
fn main() {
println!("{:?}", double("21"));
println!("{:?}", double("x"));
}
rustc --edition 2024 question.rs
./question
Ok(42)
Err("not a number")
The two layers share the string error type here, so no conversion is needed, and the question mark on each line is the same operator. The next sections make the types different and show what changes.
Box<dyn Error> at the top
A top-level function often has to accept several error types. Box<dyn Error> holds any of them, and the question mark converts each one into the box with the standard implementation.
The program reads a number from a file through a boxed error. The file exists, so the result is Ok with the number.
use std::error::Error;
use std::fs;
fn read_length(path: &str) -> Result<usize, Box<dyn Error>> {
let text = fs::read_to_string(path)?;
let number: usize = text.trim().parse()?;
Ok(number)
}
fn main() {
println!("{:?}", read_length("number.txt"));
}
rustc --edition 2024 box_dyn.rs
./box_dyn
Ok(42)
The box costs one allocation on the error path and nothing on the success path. The lesson on Box explains the allocation.
From between two error types
Two concrete error types need an impl From for the question mark to cross the boundary. The implementation usually wraps the lower error, which keeps the cause for a report.
The program parses a good value and a bad one through the wrapper. The bad case prints the parse message inside the application error.
use std::num::ParseIntError;
#[derive(Debug)]
enum LoadError {
Parse(ParseIntError),
}
impl From<ParseIntError> for LoadError {
fn from(error: ParseIntError) -> Self {
LoadError::Parse(error)
}
}
fn load(text: &str) -> Result<i32, LoadError> {
let value: i32 = text.parse()?;
Ok(value)
}
fn main() {
println!("{:?}", load("7"));
match load("x") {
Ok(value) => println!("ok {value}"),
Err(LoadError::Parse(error)) => println!("error: {error}"),
}
}
rustc --edition 2024 context.rs
./context
Ok(7)
error: invalid digit found in string
The wrapper is one line of code per lower error type, and it is the only place that knows the mapping. The lesson on error types builds the enum it wraps.
main that returns an error
main may return a Result, and then it can use the question mark like any other function. A final Err prints the debug form and gives status one, which is enough for a script.
The program parses a good value and a bad one. The first prints, the second ends main with the error and the runtime prints it.
use std::error::Error;
use std::num::ParseIntError;
fn parse(text: &str) -> Result<i32, ParseIntError> {
text.parse()
}
fn main() -> Result<(), Box<dyn Error>> {
println!("{}", parse("42")?);
println!("{}", parse("x")?);
Ok(())
}
rustc --edition 2024 main_box.rs
./main_box
echo "status: $?"
42
Error: ParseIntError { kind: InvalidDigit }
status: 1
The debug form of the error appears because main prints with Debug, not Display. The lesson on panics compares this with a crash.
A missing conversion
Without a From between the two error types, the question mark cannot convert and reports E0271: the error of the expression does not match the error of the function. The message names both types.
The program tries to convert a parse error into an application error with no implementation. The fix is the From block, or a map_err at the call.
#[derive(Debug)]
struct MyError;
fn parse(text: &str) -> Result<i32, MyError> {
let value: i32 = text.parse()?;
Ok(value)
}
fn main() {
println!("{:?}", parse("42"));
}
rustc --edition 2024 mismatch_err.rs
error[E0271]: type mismatch resolving `<i32 as FromStr>::Err == MyError`
--> mismatch_err.rs:5:27
|
5 | let value: i32 = text.parse()?;
| ^^^^^ expected `MyError`, found `ParseIntError`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0271`.
The E0271 code is specific to the question mark's conversion, which is why it is not the E0308 of a plain assignment. The From implementation is the missing sentence.
Exercises
- Add a second layer to the first program and keep the question mark in both.
- Replace the boxed error in main with a concrete error and add the conversion it needs.
- Make the program print the source chain of the wrapped error.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the question mark do on Err?
Answer
Returns the error from the current function
The operator is an early return that keeps the error in the return type.
-
What does
Box<dyn Error>allow a function to do?Answer
Return several different error types
The box erases the concrete error type so any implementation can go in it.
-
What enables the question mark between two concrete error types?
Answer
An impl From between them
The operator converts with From, so the implementation is the bridge.
-
What status does main leave after returning Err?
Answer
One
The Termination implementation reports failure with status one.
-
What does this program print?
fn parse(text: &str) -> Result<i32, String> { let value: i32 = text.parse().map_err(|_| String::from("bad"))?; Ok(value) } fn main() { println!("{:?}", parse("7")); println!("{:?}", parse("x")); }Answer
Ok(7) / Err("bad")
The first parse succeeds and the second is replaced by the message.
-
What does this program print?
fn double(text: &str) -> Result<i32, String> { let value: i32 = text.parse().map_err(|_| String::from("no"))?; Ok(value * 2) } fn main() { println!("{:?}", double("21").unwrap()); }Answer
42
The parse succeeds, the value doubles, and unwrap prints the inner number.
-
What does this program print?
use std::error::Error; fn read(text: &str) -> Result<i32, Box<dyn Error>> { let value: i32 = text.parse()?; Ok(value) } fn main() { println!("{:?}", read("9")); }Answer
Ok(9)
The parse succeeds and the result prints in the debug form.
-
What does this program print?
fn parse(text: &str) -> Result<i32, String> { text.parse().map_err(|_| String::from("bad")) } fn main() -> Result<(), String> { println!("{}", parse("5")?); println!("{}", parse("x")?); Ok(()) }Answer
5
The first print runs and the second error ends main, which prints the message to stderr.
-
What is the first line of the compiler output for this program?
#[derive(Debug)] struct MyError; fn parse(text: &str) -> Result<i32, MyError> { let value: i32 = text.parse()?; Ok(value) } fn main() { println!("{:?}", parse("42")); }Answer
error[E0271]: type mismatch resolving
<i32 as FromStr>::Err == MyErrorThe question mark cannot convert the parse error into MyError without a From implementation.
-
What does this program print?
fn check(value: i32) -> Result<i32, String> { if value < 0 { return Err(String::from("negative")); } Ok(value) } fn main() { println!("{:?}", check(-1)); println!("{:?}", check(3)); }Answer
Err("negative") / Ok(3)
The negative value produces the error and the positive one is accepted.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev