Rust: Early Return on Error Operator (?)

2024-07-08

The ? Operator: Early Return on Error

The ? operator is a powerful tool in Rust’s error handling toolkit. It can only be used within functions that return a Result. It significantly reduces the amount of boilerplate error-handling code you need to write which makes the code more readable and maintainable. It also forces you to think about and handle errors at each step. It essentially does the following:

  1. Checks for Errors: If the expression on the left of ? evaluates to an Ok value (a successful result), the value inside Ok is extracted and used as the result of the entire expression.

  2. Returns Early on Error: If the expression on the left evaluates to an Err value (an error), the ? operator does two things:

    • It automatically converts the Err into the error type expected by the function’s return type.
    • It immediately returns from the function with that converted error value.

Example 1: Parsing a Number

use std::num::ParseIntError; // Import the error type

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> { // Function returning a Result
    let num: i32 = s.parse()?;  // Parse string to i32, early return on error
    Ok(2 * num)   // Double the number if parsing succeeds
}

fn main() {
    match parse_and_double("10") {
        Ok(n) => println!("Parsed number: {}", n),
        Err(e) => println!("Error parsing: {}", e),
    }
}

In this example, if s.parse() fails, the parse_and_double function immediately returns with the ParseIntError, and the Ok(2 * num) line is never executed.

Example 2: Opening a File

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

fn read_file_contents(filename: &str) -> Result<String, Error> {
    let mut file = File::open(filename)?;  // Open file, early return on error
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;  // Read file, early return on error
    Ok(contents)
}

Here, if either File::open or file.read_to_string fails, the function returns early with the corresponding std::io::Error.

Example 3: Chaining Operations

fn process_data(input: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let num = input.parse::<i32>()?; // Parse, early return on error
    let result = some_complex_calculation(num)?; // Do some work, early return on error
    Ok(result)
}

In this example, the ? operator is used multiple times to handle errors gracefully at each step of the data processing pipeline.