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 yieldSome(value)
orNone
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. TheErr(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 anOption
insideResult
).
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 theOk
value, leavingErr
untouched.map_err
: Transforms theErr
value, leavingOk
untouched.and_then
: IfOk
, calls a closure with the value. The closure must return a newResult
. IfErr
, propagates theErr
. Useful for sequencing fallible operations.or_else
: IfErr
, calls a closure with the error. The closure must return a newResult
. IfOk
, propagates theOk
. Useful for trying alternative operations on failure.unwrap_or
: Returns theOk
value or a provided default value ifErr
.unwrap_or_else
: Returns theOk
value or computes a default value from a closure ifErr
.
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 insideOk
. If theResult
isErr
, it panics.expect(message: &str)
: Similar tounwrap
, but panics with the provided custom message if theResult
isErr
.
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
:
- Prototypes/Examples: Quick and dirty code where explicit error handling is deferred.
- Tests: Asserting that an operation must succeed in a test scenario.
- Logical Guarantees: When program logic ensures the
Result
cannot beErr
(orOption
cannot beNone
). 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.