Error handling with Result & Option in Rust
As seen before, panicking when the program is running is unideal and should be avoided if
necessary. Rust includes two commonly used enums that help ensure data is valid in runtime:
Result
and Option
.
Both types ensure invalid data and errors are handled adequately and do not cause the program to panic.
Using Option
Option is an enum that contains two variants - None
and Some
:
enum Option<T> {
None,
Some(T),
}
The T
here may be a new sight to behold. This is a generic type parameter, which will be
covered in module six. For now, know it means that any type can be within Some
- it doesn't
matter what it is, just that something is there.
A concrete example would be attempting to access an empty array. Instead of panicking and attempting
to access an index that doesn't exist, we can return an Option
// An example of a function that returns an Option, aka, Some(i32) or None.
// Notice the angled brackets which contain the type we're expecting.
fn safe_access(index: usize, slice: &[i32]) -> Option<i32> {
// We check to see if the length of the slice is zero, or
// less than the requested index. If it is, we return `None`
if slice.len() == 0 || slice.len() < index {
return None;
}
// Otherwise, we're good to return the requested item!
Some(slice[index])
}
None
does the opposite. If the Option is None
, it implies the data does not exist. This is
useful for checking the state of some data but does not describe any error associated with a
potential failure.
let empty_array = [];
let valid_array = [1, 2, 3];
// Pass it in as a reference, as per the function signature
safe_access(1, &empty_array); // returns None
safe_access(1, &valid_array); // returns Some(2)
Pattern Matching with Option
Because Option
returns an enum, it would be good practice to ensure that we handle both variants.
Functions can be called and matched directly:
let empty_array = [];
let valid_array = [1, 2, 3];
// Technically could be None or Some
// hint: look at the type of this variable
let maybe_value: Option<i32> = safe_access(1, &valid_array);
// However, let's match the function directly:
match safe_access(1, &valid_array) {
Some(value) => println!("We have a value: {value}"),
None => println!("It doesn't exist :()")
};
Using if let
with Option
Alternatively, an if let
statement may be used instead of match
. if let
essentially will
perform the same type of pattern matching, where it will look for Some
value, assign it to a
variable if it exists, and safely unwrap
it:
let valid_array = [1, 2, 3];
if let Some(value) = safe_access(0, &valid_array) {
println!("{value}"); // 1
} else {
println!("Nothing valid was found!");
}
Using Result
A Result also contains two variants, Ok(T)
and Err(E)
. While the generic T
still implies any
value, the generic E
can be used to define a custom error type, allowing us to describe why a
particular value or scenario did not output as expected.
It is used very similarly to Option
at times, with the exception that instead of returning None
,
it returns an Error
that describes what went wrong. Usually, Option
should be used when a value
is not certain to be existent or not, and Result
used when referring to a computation that has the
possibility of failing.
You may read more on Option
vs. Result
here.
enum Result<T, E> {
Ok(T),
Err(E),
}
Here is the example above, only adapted to use a Result
:
fn safe_access_result(index: usize, slice: &[i32]) -> Result<i32, String> {
if slice.len() == 0 || slice.len() < index {
return Err(String::from("Error! Invalid index!"));
}
Ok(slice[index])
}
A key aspect is the E
, or Err
, part of this expected return type. It's a String, meaning while
it enables us to express why it has failed, it is unideal.
Defining a Custom Error Type For Result
Rather than having obscure Strings as an Error type, we can instead define a custom enum to represent any potential errors:
enum SafeArrayError {
InvalidIndex
}
With this type defined, we can now change our function to the following:
enum SafeArrayError {
InvalidIndex
}
fn safe_access_result(index: usize, slice: &[i32]) -> Result<i32, SafeArrayError> {
if slice.len() == 0 || slice.len() < index {
// Now, we return a context-specific error.
return Err(SafeArrayError::InvalidIndex);
}
Ok(slice[index])
}
Try it yourself
What's happening here?
There are two functions, safe_access
and safe_access_result
. These functions both accomplish the
same task - ensure a particular element of an array can be accessed safely. A custom error type is
also defined, SafeArrayError
, which is utilized within the safe_access_result
function.