3.3 KiB
Handling Multiple Possible Errors in Rust
Background
Rust handles errors better than any other language I've ever used. All possible errors are known at build time, and by design Rust forces you to at a bare minimum acknowledge that these errors are possible with .unwrap()
, otherwise writing specific error handling logic.
The Result
enum
All of this is thanks to the Result
enum, which is defined as follows:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
is a generic type that requires two type parameters, T
, the actual value the result is wrapping, and E
, the error type in case something goes wrong. In the success case, the Ok(T)
variant will be used, and in an error case, Err(E)
will be used.
What makes this powerful is the fact that since Result
wraps the actual result value, access to that value is prevented unless the possibility of an error is acknowledged.
Handling Result
For the sake of example, say we're writing a theoretical function, acquire_banana
, that drives to the store, purchases a banana, and returns the banana.
We have a function called drive_to_store
. It returns a Result
that can either be nothing ()
, or a NavigationError
.
fn drive_to_store() -> Result<(), NavigationError> { ... }
We then have the following function, acquire_banana
. It also returns a Result
, but this time it can either be a Banana
or a PurchaseError
.
fn purchase_banana() -> Result<Banana, PurchaseError> { ... }
In other languages, we might write the function something like this:
fn acquire_banana() -> Banana {
drive_to_store();
purchase_banana()
}
However, this doesn't work in Rust. There are two issues here. First, while drive_to_store();
is in theory valid code (it returns an enum that we ignore), Result=s *must* be handled. Otherwise, you will get compile warnings. Second, we cannot return =purchase_banana
from the function, because its return type is Result<Banana, PurchaseError>
, not Banana
.
The naïve solution to this would be using the unwrap
method on the =Result=s, like this:
fn acquire_banana() -> Banana {
drive_to_store().unwrap();
purchase_banana().unwrap()
}
The .unwrap()
method does exactly what it sounds like. If a Result
is the Ok(T)
variant, it returns T
. However, the naïvety here lies in what happens on the Err(E)
variant. In that case, it panics, printing the error and kills the thread/program. A well-designed program should take errors into account, so this solution is unacceptable.
The best solution is in this case is error propagation using the question mark ?
operator. It works similarly to .unwrap()
, but instead of panicking on the Err(E)
case, it returns Err(E)
from the function. This lets you quickly unwrap results without convoluted match
statements.
This lets you convert this:
let x = match get_result() {
Ok(value) => value,
Err(error) => return Err(error),
};
Into this:
let x = get_result()?;