Want to contribute? Fork me on Codeberg.org!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
blog.elnu.com/content/posts/handling-multiple-possible-...

3.1 KiB

title date tags draft
Handling Multiple Possible Errors in Rust 2022-08-04
programming
true

Background

Rust handles errors better than any other language Ive 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 were 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 doesnt 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), Results 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 Results, 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()?;