Recoverable Error Handling with Result
Recipe | Crates |
---|---|
Handle Recoverable Errors with Result | |
Retrieve or Transform Values in Result | |
Get Reference to Values Inside a Result | |
Choose between Result and Option |
Handle Recoverable Errors with Result
Result<T, E>
↗ is an enum
very frequently used for error handling - specifically when errors are expected and recoverable.
Result<T, E>
has two variants:
Ok(T)
↗ represents success and contains a value of typeT
.Err(E)
↗ represents an error and contains an error value of typeE
.
E
above often implements the std::error::Error
↗ trait, which is a standard trait for reporting errors in Rust. You can define your own error types by implementing this trait (but this is verbose) or use libraries like thiserror
or anyhow
to greatly simplify error handling.
Result
is annotated with the #[must_use]
attribute: the compiler issues a warning when a Result
value is ignored, prompting you if you forgot to handle an error.
The following example show to return Result
from a function, propagate it to the caller with the ?
operator, or process it with match
:
//! `Result` is a type that represents either success (`Ok`) or failure (`Err`): //! //! ``` //! pub enum Result<T, E> { //! Ok(T), //! Err(E), //! } //! ``` use std::fs::File; use std::io; use std::io::Read; use std::num::ParseIntError; fn open_file(file_path: &str) { // Faillible functions like `File::open` return a `Result`... let open_result: Result<File, io::Error> = File::open(file_path); // We could handle their `Result` there and then... match open_result { Err(e) => eprintln!("An error occurred while opening the file: {e}"), Ok(file) => println!("Opened {file:?}"), } } // ...but most often you'll want to return early when encountering an error, // and propagate the error up the call stack. #[allow(clippy::question_mark)] fn read_file(file_path: &str) -> Result<String, io::Error> { let mut file = match File::open(file_path) { Ok(file) => file, Err(e) => return Err(e), // Early return. }; // This said, having to use multiple `match` or `if let` is verbose // when chaining calls to faillible methods... let mut contents = String::new(); if let Err(e) = file.read_to_string(&mut contents) { Err(e) } else { Ok(contents) } } // Therefore, use the `?` operator as a shortcut to return early // in case of an error. The following is equivalent to the previous function. fn read_file2(file_path: &str) -> Result<String, io::Error> { let mut file: File = File::open(file_path)?; // Note that `file` is of type `File`, // not `io::Result<File> = Result<File, io::Error>`. let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } // You can even chain method calls immediately after the `?`, e.g. // `File::open(file_path)?.read_to_string(&mut contents)?;`. // You will often need to return one of multiple `Result` types. // You could create a custom `MyError` error to do so: fn read_and_parse_file(file_path: &str) -> Result<i32, MyError> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; // `read_to_string` returns `Result<_, io::Error>`. let number = contents.trim().parse()?; // `parse` returns `Result<_, std::num::ParseIntError>`. Ok(number) } #[allow(dead_code)] #[derive(Debug)] enum MyError { Io(io::Error), Parse(ParseIntError), } impl std::error::Error for MyError {} // Implementing the `Error` trait requires that `Debug` and `Display` are // implemented too. impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::Io(err) => write!(f, "IO error: {err}"), MyError::Parse(err) => write!(f, "Parse error: {err}"), } } } // We should also implement the `From` trait: impl From<io::Error> for MyError { fn from(err: io::Error) -> MyError { MyError::Io(err) } } impl From<ParseIntError> for MyError { fn from(err: ParseIntError) -> MyError { MyError::Parse(err) } } // The above boilerplate can be avoided with the `thisError` crate, // which provides a convenient `derive` macro // for the standard library's `std::error::Error` trait. // Use it when writing libraries, in which case custom error types are // recommended. #[allow(dead_code)] #[derive(thiserror::Error, Debug)] enum MyError2 { #[error("Io error")] Io(#[from] io::Error), #[error("Parse error")] Parse(#[from] ParseIntError), } // A simpler method than returning a custom error type // may be to return a trait object (`dyn Error`, typically wrapped in a `Box`) // at the cost of greater opacity... fn read_and_parse_file2( file_path: &str, ) -> Result<i32, Box<dyn std::error::Error>> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let number = contents.trim().parse()?; Ok(number) } // ...but crates like `anyhow` can simplify error management a great deal... // `anyhow::Result<T>` is equivalent to `std::result::Result<T, anyhow::Error>`. // Use it in applications, since custom error types are rarely needed then. fn read_and_parse_file3(file_path: &str) -> anyhow::Result<i32> { let mut file = File::open(file_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let number = contents.trim().parse()?; Ok(number) } // A function without return value in truth returns `Unit`: // `fn func() {...}` is a shorthand for `fn func() -> () { ... }` // To convert it to a faillible function, return `Result<(), SomeErrorType>`. fn unit_return_value(file_path: &str) -> Result<(), anyhow::Error> { let _i: i32 = read_and_parse_file3(file_path)?; println!("I don't return anything!"); // Do not forget to return a `Result` in the happy path! // The double parentheses are required. It is really `Ok( () )`. Ok(()) } // `main()` can return a `Result`, more precisely a `Result<T, E> where T: // Termination, E: Debug)`. `Result<(), Box<dyn std::error::Error>>` or // `anyhow::Result<()>` are common choices. fn main() -> Result<(), Box<dyn std::error::Error>> { let file_path = "example.txt"; open_file(file_path); // Here we ignore the `Result` return values // to avoid returning early and to exercise all the code. // You will rarely need to do this. let _ = read_file(file_path); let _ = read_file2(file_path); let _ = read_and_parse_file(file_path); let _ = read_and_parse_file2(file_path); let _ = read_and_parse_file3(file_path); let _ = unit_return_value(file_path); Ok(()) }
Retrieve or Transform Values in Result
In addition to working with pattern matching, Result
↗ provides a wide variety of convenience methods, the most common being:
Method | Description | Use Case |
---|---|---|
unwrap() ↗ | Returns the Ok value or panics if Err . | Use when you're certain the result is Ok . See also expect . |
unwrap_or(default) ↗ | Returns the Ok value or a default if Err . | Use when you want a fallback value. |
unwrap_or_else(func) ↗ | Returns the Ok value or calls a function to generate a fallback. | Use when computing a fallback dynamically. |
map(func) ↗ | Applies a function to the Ok value. | Use to transform the result, if successful. |
map_err(func) ↗ | Applies a function to the Err value. | Use to transform the error. |
and_then(func) ↗ | Applies a function that returns Result , flattening the result. | Use for chaining operations that may fail. |
or_else(func) ↗ | Calls a function to provide an alternative Result , if Err . | Use to recover from errors dynamically. |
map_or(default, func) ↗ | Applies a function to the Ok value or returns a default if Err . | Use when you need a fallback value. |
map_or_else(err_func, ok_func) ↗ | Applies a function to the Ok value or calls another function for Err . | Use when you need dynamic error handling. |
is_ok() ↗ | Returns true if the result is Ok . | Use to check success. |
is_err() ↗ | Returns true if the result is Err . | Use to check failure. |
The following example demonstrates a few of these methods:
//! Example of `Result` methods for value transformation and control flow. // A contrived example of a function returning a `Result`: fn divide(a: i32, b: i32) -> Result<i32, &'static str> { if b == 0 { Err("Cannot divide by zero.") } else { Ok(a / b) } } #[allow(clippy::bind_instead_of_map)] fn main() { let result = divide(10, 2) // Transform the inner value if `Ok`: .map(|x| x * 2) // Chain another `Result`-returning operation: .and_then(|x| Ok(x + 1)); // In rare cases, you may want to panic with a custom message if `Err`. // This converts a recoverable error into an irrecoverable one: result.expect("Computation failed."); // `expect` is preferred over `unwrap`: result.unwrap(); // It is recommended to check result status before unwrapping, // or use an `if let` expression instead: println!("Is result `Ok`? {}", result.is_ok()); println!("Is result `Err`? {}", result.is_err()); if let Ok(ref value) = result { println!("Result value: {value}"); } else { println!("Computation failed."); } // You can also provide a default value if `Err`: println!("Final value: {}", result.unwrap_or(-1)); // You could also write: .or_else(|_| Ok(-1)); }
Get Reference to Values Inside a Result
The following example demonstrates the usage of the as_ref()
, as_deref()
, and as_mut()
methods on the Result
type:
as_ref()
↗ converts&Result<T, E>
intoResult<&T, &E>
, allowing access to references.as_deref()
↗ converts&Result<T, E>
intoResult<&U, &E> where T: Deref<Target = U>
, useful for convertingResult<String, E>
toResult<&str, E>
.as_mut()
↗ converts&mut Result<T, E>
intoResult<&mut T, &mut E>
, allowing mutation of the inner value.
These methods are useful for working with references to the values inside a Result
without consuming the Result
itself.
//! Demonstrates the use of `as_ref`, `as_deref`, and `as_mut` methods on //! `Result`. fn get_message(success: bool) -> Result<String, &'static str> { if success { Ok(String::from("Rust")) } else { Err("Failed to get message") } } fn main() { let result: Result<String, &'static str> = get_message(true); // `as_ref` converts from `&Result<T, E>` to Result<&T, &E>`. let ref_result: Result<&String, &&str> = result.as_ref(); println!("as_ref(): {ref_result:?}"); // Use `as_deref` to coerce the `Ok` variant of the original `Result` via // `Deref`. Here, `&String` is coerced to `&str`. let deref_result: Result<&str, &&str> = result.as_deref(); println!("as_deref(): {deref_result:?}"); // Use `as_mut` to get a mutable reference: let mut result_mut = get_message(true); let mut_ref: Result<&mut String, &mut &str> = result_mut.as_mut(); if let Ok(s) = mut_ref { s.push_str(" is awesome!"); } println!("as_mut(): {result_mut:?}"); // There is also a `as_deref_mut` method. }
Choose between Result
and Option
Option
↗ represents an optional value. Use Option
when a value might be missing but isn't necessarily an error (e.g., searching for an item in a list).
Result<T, E>
↗ represents the outcome of an operation that can succeed or fail. Use Result
when an operation can fail and you need to handle errors explicitly (e.g., file I/O operations).
The following table lists common methods that convert Result<T, E>
to Option
:
| Method | Description | Use Case |
| ok()
↗ | Converts Result<T, E>
into Option<T>
, discarding the error. | Use when you only care about the success value. |
| err()
↗ | Converts Result<T, E>
into Option<E>
, discarding the success value. | Use when you only care about the error. |
See also the Option chapter.
References
Related Topics
- Iterators.
- Optional Values.
- Pattern Matching.