Option, Result and Error Handling · about 40 min

The ? Operator and Propagating Errors

Why this matters

Most functions that can fail do so because something they call failed: reading a config fails because opening the file failed, parsing a record fails because one field did not parse. The usual response is to propagate the error to the caller, who knows better what to do. Writing a match for every call buries the actual logic under error plumbing. The ? operator does the plumbing in one character, which is why idiomatic Rust error handling is both explicit and concise.

Propagating by hand

Here is a function that reads two numbers from text and adds them, with errors propagated manually:

use std::num::ParseIntError;

fn add_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = match a.trim().parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    let y = match b.trim().parse::<i32>() {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

fn main() {
    println!("{:?}", add_strs("2", " 40"));
    println!("{:?}", add_strs("2", "forty"));
}
Ok(42)
Err(ParseIntError { kind: InvalidDigit })

Every fallible call follows the same shape: "if Ok, give me the value; if Err, return it".

The ? operator

? is exactly that shape:

use std::num::ParseIntError;

fn add_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.trim().parse::<i32>()?;
    let y = b.trim().parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", add_strs("2", " 40"));
    println!("{:?}", add_strs("2", "forty"));
}
Ok(42)
Err(ParseIntError { kind: InvalidDigit })

expr? on a Result:

It works on Option too (None returns None), as you saw in lesson 1. It can be chained: s.split_once(',').ok_or("no comma")?.0.trim().parse::<i32>()? reads left to right as a sequence of steps, any of which may bail out.

? only works in functions that return Result (or Option)

? needs somewhere to return the error to:

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

E0277: main returns (), so there is no error to return. The fix the compiler suggests is the next topic.

main can return Result

main may return Result<(), E> for any error type that can be printed with Debug. If it returns Err, Rust prints Error: followed by the Debug form of the error to stderr and exits with status 1:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let n: i32 = "5".parse()?;
    println!("parsed {n}");
    let m: i32 = "five".parse()?;
    println!("never printed {m}");
    Ok(())
}
parsed 5
Error: ParseIntError { kind: InvalidDigit }

Box<dyn Error> means "any error type at all, stored on the heap". dyn Error is a trait object (module 10); Box is a heap pointer (module 13). For now, Result<(), Box<dyn Error>> is the standard "anything can go wrong here" return type for main and for small programs: ? converts every standard error type into it automatically. At the end of a successful main, return Ok(()) (() is the unit value: "nothing interesting").

The printed message uses Debug formatting, which is fine for tools you use yourself. For polished user-facing messages, match the error in main, print it nicely with eprintln! and call std::process::exit.

? converts errors with From

When a function calls operations with different error types, it needs one error type for its own signature. ? helps: before returning an error, it calls From::from on it, so any error type that can be converted into the function's error type works. Box<dyn Error> can be created from every standard error, which is why it is so convenient:

use std::error::Error;
use std::fs;

fn read_number(path: &str) -> Result<i64, Box<dyn Error>> {
    let text = fs::read_to_string(path)?;
    let n = text.trim().parse::<i64>()?;
    Ok(n * 2)
}

fn main() {
    match read_number("/no/such/file") {
        Ok(n) => println!("{n}"),
        Err(e) => println!("failed: {e}"),
    }
    match read_number("/proc/sys/kernel/pid_max") {
        Ok(n) => println!("twice pid_max: {n}"),
        Err(e) => println!("failed: {e}"),
    }
}
failed: No such file or directory (os error 2)
twice pid_max: 8388608

fs::read_to_string fails with an io::Error, parse with a ParseIntError; both convert to Box<dyn Error>. Printing the boxed error with {} shows the underlying error's message. (The second call reads a Linux kernel setting exposed as a file; the value depends on the machine.) Lesson 4 builds a custom error type with its own From conversions, which is what libraries do.

Turning Option into Result for ?

In a function returning Result, you cannot ? an Option directly (there is no error value). Convert it with ok_or (or ok_or_else, which builds the error lazily):

fn first_word_len(s: &str) -> Result<usize, String> {
    let w = s.split_whitespace().next().ok_or("empty input")?;
    Ok(w.len())
}

fn main() {
    println!("{:?}", first_word_len("hello world"));
    println!("{:?}", first_word_len("   "));
}
Ok(5)
Err("empty input")

"empty input" is a &str; ? converts it to the String error type via From<&str> for String.

Adding context with map_err

Raw errors often lack context ("invalid digit found in string": which string?). map_err replaces or wraps an error before ? propagates it:

fn parse_field(name: &str, value: &str) -> Result<u32, String> {
    let n = value.trim().parse::<u32>().map_err(|e| format!("field {name}: {value:?}: {e}"))?;
    Ok(n)
}

fn main() {
    println!("{:?}", parse_field("age", "41"));
    println!("{:?}", parse_field("age", "4l"));
}
Ok(41)
Err("field age: \"4l\": invalid digit found in string")

Common mistakes

Recap

Going further: read the "Recoverable Errors with Result" chapter of the Rust Book (9.2), in particular the section "A Shortcut for Propagating Errors: the ? Operator".

Exercises

Exercise 1 · write code

Propagate with ?

Write fn parse_point(s: &str) -> Result<(i32, i32), String> for input like 3,-4:

Use ok_or and map_err with ? so the function body has no match. The given main prints each stdin line's result:

(3, -4)
error: missing comma
error: bad number: x

for the lines 3,-4, 5, x,2.

File: main.rs

Starting point

use std::io;

fn parse_point(s: &str) -> Result<(i32, i32), String> {
    Err(String::from("todo"))
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        match parse_point(line.trim()) {
            Ok(p) => println!("{:?}", p),
            Err(e) => println!("error: {e}"),
        }
    }
}

Example

input

3,-4
5
x,2

expected output

(3, -4)
error: missing comma
error: bad number: x
Hint 1

s.split_once(',').ok_or("missing comma".to_string())? gives you the two halves or returns the error.

Hint 2

For each half: half.trim().parse::<i32>().map_err(|_| format!("bad number: {}", half.trim()))?

Hint 3

Finish with Ok((x, y)).

Reference solution
use std::io;

fn parse_num(s: &str) -> Result<i32, String> {
    let s = s.trim();
    s.parse::<i32>().map_err(|_| format!("bad number: {s}"))
}

fn parse_point(s: &str) -> Result<(i32, i32), String> {
    let (a, b) = s.split_once(',').ok_or("missing comma".to_string())?;
    let x = parse_num(a)?;
    let y = parse_num(b)?;
    Ok((x, y))
}

fn main() {
    for line in io::stdin().lines() {
        let line = line.unwrap();
        match parse_point(line.trim()) {
            Ok(p) => println!("{:?}", p),
            Err(e) => println!("error: {e}"),
        }
    }
}

Exercise 2 · write code

main that returns Result

Make main return Result<(), Box<dyn std::error::Error>> and use ? for every fallible step, so the program needs no unwrap. It reads two lines from stdin, a count n (u32) and a word, and prints the word n times on one line separated by spaces:

hi hi hi

for 3 and hi. For a bad count, ? makes main return the error, which Rust prints to stderr as Error: ParseIntError { kind: InvalidDigit } and exits with status 1.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
    let mut word = String::new();
    io::stdin().read_line(&mut word).unwrap();
    let words = vec![word.trim(); n as usize];
    println!("{}", words.join(" "));
}

Example

input

3
hi

expected output

hi hi hi

Example

input

0
hi

expected output

Hint 1

Signature: fn main() -> Result<(), Box<dyn std::error::Error>>, and end the body with Ok(()).

Hint 2

Replace each .unwrap() with ?.

Reference solution
use std::error::Error;
use std::io;

fn main() -> Result<(), Box<dyn Error>> {
    let mut line = String::new();
    io::stdin().read_line(&mut line)?;
    let n: u32 = line.trim().parse()?;
    let mut word = String::new();
    io::stdin().read_line(&mut word)?;
    let words = vec![word.trim(); n as usize];
    println!("{}", words.join(" "));
    Ok(())
}

Exercise 3 · quiz

What does ? do?

Inside fn load() -> Result<Config, AppError>, what does the expression read_file(path)? do, where read_file returns Result<String, io::Error>?

Answer
  • If Ok(s), evaluates to s; if Err(e), returns Err(AppError::from(e)) from load immediately

? unwraps the success value or returns early with the error, first converting it with From::from into the function's error type. So it compiles when AppError implements From<io::Error>. That conversion is what lets one function call several operations with different error types and still return a single error type.