15.3 Propagating Errors with the ? Operator

Handling errors from multiple sequential operations using match or combinators can still become nested or verbose. Rust provides the question mark operator (?) as syntactic sugar for the common pattern of error propagation.

15.3.1 How ? Works

When applied to an expression returning Result<T, E>, the ? operator behaves as follows:

  1. If the Result is Ok(value), it unwraps the Result and yields the value for the rest of the expression.
  2. If the Result is Err(error), it immediately returns the Err(error) from the enclosing function.

Crucially, the ? operator can only be used inside functions that themselves return a Result (or Option, or another type implementing specific traits). The error type (E) of the Result being questioned must be convertible into the error type returned by the enclosing function (via the From trait, discussed later).

Consider reading a username from a file, simplified using ?:

use std::fs::File;
use std::io::{self, Read};

// This function must return Result because it uses '?'
fn read_username_from_file() -> Result<String, io::Error> {
    // File::open returns Result<File, io::Error>.
    // If Ok, the File handle is assigned to `file`.
    // If Err, the io::Error is returned immediately from read_username_from_file.
    let mut file = File::open("username.txt")?;

    let mut s = String::new();
    // file.read_to_string returns Result<usize, io::Error>.
    // If Ok, the number of bytes read (usize) is discarded, and `s` contains content
    // If Err, the io::Error is returned immediately from read_username_from_file.
    file.read_to_string(&mut s)?;

    // If both operations succeeded, wrap the string in Ok and return it.
    Ok(s)
}
// Dummy main for context
fn main() {
    match read_username_from_file() {
        Ok(name) => println!("Username: {}", name),
        Err(e) => eprintln!("Error: {}", e),
    }
}

This use of ? is equivalent to manually writing a match for each operation that checks for Err and returns early, or extracts the Ok value otherwise. The ? operator makes this common pattern significantly more readable and concise. It directly expresses the intent: “Try this operation; if it fails, propagate the error; otherwise, continue with the successful result.”

15.3.2 Chaining ?

The power of ? becomes even more apparent when operations are chained:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
// The entire operation can be condensed further.
fn read_username_from_file_chained() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("username.txt")?.read_to_string(&mut s)?; // Chained '?'
    Ok(s)
}

// Even more concisely using standard library functions:
fn read_username_from_file_stdlib() -> Result<String, io::Error> {
    std::fs::read_to_string("username.txt") // This function uses '?' internally
}
}
}

15.3.3 Returning Result from main

The main function, which typically returns (), can also be declared to return Result<(), E> where E is any type implementing the std::error::Error trait. This allows using the ? operator directly within main for cleaner error handling in simple applications.

use std::fs::File;
use std::io::Read;
use std::error::Error; // Required trait for the error type returned by main

fn main() -> Result<(), Box<dyn Error>> { // Return Box<dyn Error> for simplicity
    let mut file = File::open("config.ini")?; // If open fails, main returns Err
    let mut contents = String::new();
    file.read_to_string(&mut contents)?; // If read fails, main returns Err
    println!("Config content:\n{}", contents);
    Ok(()) // Indicate successful execution
}

If main returns Ok(()), the program exits with a status code 0. If main returns an Err(e), Rust prints the error description (using its Display implementation) to standard error and exits with a non-zero status code. Using Box<dyn Error> is a convenient way to allow different error types to be propagated out of main (discussed next).