Result and the question mark

In Rust Result<T, E> is the enum for an operation that may fail with a reason: Ok(value) or Err(error). It is the counterpart of Option for the case where the caller needs to know why something went wrong, and the question mark is the operator that passes the error up without a match at every step.

Coming from C++? Errors and exceptions parses a number with std::stoi and with str::parse, then lets the failure escape in both languages: one terminates the process, the other does not compile.

The lesson writes the same function with a match, with the question mark, and with main returning a Result. The last section is the error for using the question mark where there is no Result to return.

Ok and Err

The function below divides two integers and returns Err with a message when the divisor is zero. The caller matches the result twice, once for a good division and once for the failure, and prints the value or the reason.

The error type here is String, which is the simplest choice. The lesson on error types replaces it with an enum that a caller can match on.

fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { return Err(String::from("division by zero")); } Ok(a / b) } fn main() { match divide(10, 2) { Ok(value) => println!("ok {value}"), Err(message) => println!("error {message}"), } match divide(1, 0) { Ok(value) => println!("ok {value}"), Err(message) => println!("error {message}"), } }

rustc --edition 2024 ok_err.rs
./ok_err

ok 5
error division by zero

Both arms are written out, and the compiler checks that the match is complete. That is the whole difference from a function that returns a sentinel value: the failure cannot be ignored by accident.

The question mark

text.parse().map_err(...)? unwraps the Ok value and returns the Err from the whole function. The map_err converts the parse error into the string type the function promises, which is the part that surprises people first.

The program parses a number and doubles it. The first call returns Ok(42), and the second returns the custom message, which the debug print shows in the Err variant.

fn parse_number(text: &str) -> Result<i32, String> { let value: i32 = text.parse().map_err(|_| String::from("not a number"))?; Ok(value * 2) } fn main() { println!("{:?}", parse_number("21")); println!("{:?}", parse_number("x")); }

rustc --edition 2024 question.rs
./question

Ok(42)
Err("not a number")

Without the map_err, the question mark would need a From conversion between the two error types, which the lesson on error propagation uses instead. Both forms appear in real code.

main that returns Result

fn main() -> Result<(), String> may use the question mark and returns a non-zero status when it ends with an Err. The runtime prints the debug form of the error after the word Error, which is enough for a script to see why it failed.

The program checks a value twice, and the second check fails. The first line prints, the second propagates, and the shell sees status 1.

fn check(value: i32) -> Result<(), String> { if value > 10 { return Err(String::from("too large")); } println!("accepted {value}"); Ok(()) } fn main() -> Result<(), String> { check(5)?; check(12)?; Ok(()) }

rustc --edition 2024 main_result.rs
./main_result
echo "status: $?"

accepted 5
Error: "too large"

The output ends with Error: "too large", which is the debug form of the Err value. The lesson on functions shows the ExitCode alternative for a status without a message.

Changing the error type

map_err takes a closure that receives the old error and returns the new one. It is the smallest way to add context, and the closure is the subject of closures.

The program parses a string that is not a number and prefixes the error with a short explanation. The debug print shows the Err variant with the new message, which now says what the program was doing.

fn main() { let result: Result<i32, String> = "x" .parse::<i32>() .map_err(|error| format!("parse failed: {error}")); println!("{result:?}"); }

rustc --edition 2024 maperr.rs
./maperr

Err("parse failed: invalid digit found in string")

The inner text of the parse error is kept, because the closure formats the original error rather than replacing it. A context layer that drops the cause makes a failure much harder to diagnose.

The question mark in main

The question mark returns from the enclosing function, so that function must return a type that can carry the error. In a main that returns () it is E0277, and the help suggests adding a return type.

The program parses a string in a plain main. The compiler reports the operator, the missing return type and the function that would accept it, which is a complete recipe in one message.

fn main() { let value: i32 = "x".parse()?; println!("{value}"); }

rustc --edition 2024 question_err.rs

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> question_err.rs:2:33
|
1 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
2 | let value: i32 = "x".parse()?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
1 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
2 | let value: i32 = "x".parse()?;
3 | println!("{value}");
4 + Ok(())
|

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

The fix is a main that returns Result, or a match that handles the error on the spot. The lesson on panics explains why unwrap is a third option that turns the error into a crash.

Exercises

  1. Write a function that returns Result<i32, String> and fails for negative input. Call it with both kinds of value.
  2. Add context with map_err to the function and print the new message.
  3. Change main to return a Result and make the second check fail. What does the shell print for the status?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does the Err variant of a Result carry?

    Answer

    The reason the operation failed

    The error type is part of the Result, so the caller can read why it failed.

  2. What does the question mark do in a function that returns Result?

    Answer

    Returns the value on Ok and the error on Err

    It unwraps Ok and returns the Err from the enclosing function.

  3. Which return type allows main to use the question mark?

    Answer

    Result<(), E> for some error type E

    The question mark needs a function whose return type can carry the error.

  4. What does map_err change?

    Answer

    The error type, leaving the Ok value alone

    The closure runs only on the error, which is how context is added.

  5. What does this program print? fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { return Err(String::from("division by zero")); } Ok(a / b) } fn main() { match divide(10, 2) { Ok(value) => println!("ok {value}"), Err(message) => println!("error {message}"), } }

    Answer

    ok 5

    The division succeeds, and the Ok arm prints the quotient.

  6. What does this program print? fn parse_number(text: &str) -> Result<i32, String> { let value: i32 = text.parse().map_err(|_| String::from("not a number"))?; Ok(value * 2) } fn main() { println!("{:?}", parse_number("21")); println!("{:?}", parse_number("x")); }

    Answer

    Ok(42) / Err("not a number")

    The good parse is doubled and the bad one is replaced by the custom message.

  7. What does this program print? fn check(value: i32) -> Result<(), String> { if value > 10 { return Err(String::from("too large")); } println!("accepted {value}"); Ok(()) } fn main() { println!("{:?}", check(5)); println!("{:?}", check(12)); }

    Answer

    accepted 5 / Ok(()) / Err("too large")

    The first check prints and returns Ok, and the second returns the error which the debug print shows.

  8. What does this program print? fn main() { let result: Result<i32, String> = "x" .parse::<i32>() .map_err(|error| format!("parse failed: {error}")); println!("{result:?}"); }

    Answer

    Err("parse failed: invalid digit found in string")

    The closure formats the original error into the new message.

  9. What is the first line of the compiler output for this program? fn main() { let value: i32 = "x".parse()?; println!("{value}"); }

    Answer

    error[E0277]: the ? operator can only be used in a function that returns Result or Option (or another type that implements FromResidual)

    main returns the unit type, and the help suggests adding a return type.

  10. What does this program print? fn main() { let value: Result<i32, String> = Ok(7); println!("{}", value.is_ok()); println!("{}", value.is_err()); }

    Answer

    true / false

    The value is Ok, so is_ok is true and is_err is false.

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