15.2 The Result<T, E> Enum for Recoverable Errors

For most anticipated runtime failures, Rust employs the Result<T, E> enum.

15.2.1 Definition of Result

The Result enum is defined in the standard library:

enum Result<T, E> {
    Ok(T), // Represents success and contains a value of type T.
    Err(E), // Represents error and contains an error value of type E.
}
  • T: The type of the value returned in the success case (Ok variant).
  • E: The type of the error value returned in the failure case (Err variant).

A function signature like fn might_fail() -> Result<Data, ErrorInfo> clearly communicates that the function can either succeed, returning a Data value wrapped in Ok, or fail, returning an ErrorInfo value wrapped in Err. The compiler requires the caller to handle both possibilities, preventing the common C pitfall of accidentally ignoring an error return code.

15.2.2 Handling Result Values

The most fundamental way to handle a Result is with a match expression:

use std::fs::File;
use std::io;

fn main() {
    let file_result = File::open("my_file.txt"); // Returns Result<File, io::Error>

    let file_handle = match file_result {
        Ok(file) => {
            println!("File opened successfully.");
            file // The value inside Ok is extracted
        }
        Err(error) => {
            // Handle the error based on its kind
            match error.kind() {
                io::ErrorKind::NotFound => {
                    eprintln!("Error: File not found: {}", error);
                    // Decide what to do: maybe return, maybe panic, maybe create
                    // the file. For this example, we panic. In real code, avoid
                    // panic for recoverable errors.
                    panic!("File not found, cannot continue.");
                }
                other_error => {
                    eprintln!("Error opening file: {}", other_error);
                    panic!("An unexpected I/O error occurred.");
                }
            }
        }
    };

    // If we didn't panic, we can use file_handle here...
    println!("Continuing execution with file handle (if not panicked).");
    // file_handle goes out of scope here, and its destructor closes the file.
}

This match forces explicit consideration of both Ok and Err. The nested match demonstrates handling specific error kinds within the io::Error type.

Alternatively, you can check the state using methods like is_ok() and is_err() before attempting to extract the value (often via unwrap, discussed later, though careful handling is preferred):

use std::fs::File;
use std::io;
fn main() {
    let file_result = File::open("another_file.txt");

    if file_result.is_ok() {
        println!("File open seems ok.");
        // Proceed, likely unwrapping or matching to get the value
        let _file = file_result.unwrap();
    } else if file_result.is_err() {
        let error = file_result.err().unwrap(); // Get the error value
        eprintln!("Failed to open file: {}", error);
        // Handle the error appropriately
    }
}

While is_ok() and is_err() are simple checks, match or combinators are generally preferred for robust handling as they ensure both cases (Ok and Err) are considered together.

15.2.3 Option<T> vs. Result<T, E>

Rust also provides the Option<T> enum for representing optional values:

enum Option<T> {
    Some(T), // Represents the presence of a value of type T.
    None,    // Represents the absence of a value.
}

The distinction is crucial:

  • Use Option<T> when a value might be absent, and this absence is a normal, expected outcome, not an error. Example: Searching a hash map might yield Some(value) or None if the key isn’t present. None is not a failure; it’s a valid result.
  • Use Result<T, E> when an operation could fail, and you need to convey why it failed. The Err(E) variant carries information about the error condition. Example: Opening a file might fail due to permissions (Err(io::Error)), which is distinct from successfully determining a file doesn’t contain a specific configuration key (Ok(None) using an Option inside Result).

15.2.4 Combinators for Result

While match is explicit, it can be verbose for chained operations. Result provides methods called combinators that allow transforming or chaining Result values more concisely. Common combinators include:

  • map: Transforms the Ok value, leaving Err untouched.
  • map_err: Transforms the Err value, leaving Ok untouched.
  • and_then: If Ok, calls a closure with the value. The closure must return a new Result. If Err, propagates the Err. Useful for sequencing fallible operations.
  • or_else: If Err, calls a closure with the error. The closure must return a new Result. If Ok, propagates the Ok. Useful for trying alternative operations on failure.
  • unwrap_or: Returns the Ok value or a provided default value if Err.
  • unwrap_or_else: Returns the Ok value or computes a default value from a closure if Err.

Example using and_then and map:

use std::num::ParseIntError;

fn multiply_combinators(first_str: &str, second_str: &str) ->
    Result<i32, ParseIntError> {
    first_str.parse::<i32>().and_then(|first_number| {
        second_str.parse::<i32>().map(|second_number| {
            first_number * second_number
        })
    })
    // If first parse fails, and_then short-circuits, returning the Err.
    // If first succeeds, second parse is attempted.
    // If second parse fails, map propagates the Err.
    // If second succeeds, map applies the closure (multiplication) to the Ok value.
}

fn main() {
    println!("Comb. Multiply '10' and '2': {:?}", multiply_combinators("10", "2"));
    println!("Comb. Multiply 'x' and 'y': {:?}", multiply_combinators("x", "y"));
}

Many other useful combinators exist. For a comprehensive list, refer to the official std::result::Result documentation.

15.2.5 The unwrap and expect Methods (Use with Caution)

Result<T, E> (and Option<T>) have methods that provide convenient shortcuts but can cause panics:

  • unwrap(): Returns the value inside Ok. If the Result is Err, it panics.
  • expect(message: &str): Similar to unwrap, but panics with the provided custom message if the Result is Err.
fn main() {
    let result: Result<i32, &str> = Err("Operation failed");

    // let value = result.unwrap(); // Panics with a generic message
    let value = result.expect("Critical operation failed unexpectedly!");
    // Panics with specific message
    println!("Value: {}", value); // This line is never reached
}

When to use unwrap or expect:

  1. Prototypes/Examples: Quick and dirty code where explicit error handling is deferred.
  2. Tests: Asserting that an operation must succeed in a test scenario.
  3. Logical Guarantees: When program logic ensures the Result cannot be Err (or Option cannot be None). For example, accessing a default value inserted into a map just before.

Avoid unwrap and expect in production code where failure is a realistic possibility. An unexpected panic is usually less desirable and harder to debug than a properly handled Err. Prefer match, combinators, or the ? operator for robust error handling.