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:
- If the
Result
isOk(value)
, it unwraps theResult
and yields thevalue
for the rest of the expression. - If the
Result
isErr(error)
, it immediately returns theErr(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).