Rust Error Handling, Part Two

By | January 17, 2022

In the previous post, I covered a quick overview of error handling approaches in different languages. Then, I introduced Rust’s error handling using the Result<T, E> enum. This time we’ll look at how Rust’s approach improves on other techniques.

Compared to Error Returns

Returning a Result<T, E> from a function is very much like the error return approach used by earlier programming languages. Like simple error return values, error handling is pretty simple. All you need to do is check for the error return from each function that can return an error.

Unlike the traditional error return values, though, Result<T, E> fixes some disadvantages. First of all, you cannot accidentally forget to check the error value. Two features of the language support this. One, you cannot use the error return as if it were the correct value, because you need to extract the value type in order to use it. If you try to extract the T from Result<T, E> when an error was returned, the code fails. Rust also prevents you from ignoring the error return from a function (accidentally or on purpose). Rust requires that you explicitly check the result, or explicitly discard it.

// fails to compile, because the Result is not tested.
fallible_func();

// panics if the function returns an Err.
let v = fallible_func().unwrap();

// fails to compile, because returns a Result
let v: T = fallible_func();

These two features prevent accidental mishandling of returned errors.

Compared to Exceptions

The main advantage to using exceptions for error handling is that the exception is automatically propagated, instead of being dropped if you don’t handle it. The problem with this approach is that it may not be obvious from the code when an error can be propagated.

Rust handles this by explicitly returning the error from each function. Since the obvious code for detecting an error and re-returning it is rather straight-forward and repeatable, Rust supports the question mark operator (?) to simplify this common occurrence.

let value = fallible_func()?;

Given that the fallible_func() method returns a Result, the ? returns the value if the Result was Ok. If, on the other hand, the Result is an Err, ? returns the error as a Result from the calling function. This gives much the same behavior as an exception, but the return to the calling function is explicit.

One other feature of exceptions that is supported by Rust’s error handling is unconditional exit from the program. If an exception is not caught somewhere, it terminates the program. In Rust, you can do this explicitly one of two ways.

// An error return panics, terminating the program.
fallible_func().unwrap();

// An error return panics with the given message.
fallible_func().expect("Shouldn't happen, panic!");

Like all of the Rust error handling, the behavior is explicit enough that you can directly search for it in case of a panic that you did not expect.

Handling a Result

There are many approaches for handling a Result. Potentially the most obvious is pattern matching.

match fallible_func() {
    Ok(val) => use_value(val),
    Err(e) => report_error(e)
};

The Ok(val) branch covers handling the successful return of a value from fallible_func(). The Err(e) branch handles the error return from fallible_func(). Although easy to understand, this approach is a bit verbose. In some cases, it is the most appropriate.

In the last section, I showed the usage ? to automatically return the error.

The Result type also supports a number of methods that simplify some relatively obvious ways you might like to deal with an error.

// return 42 if the function returned an error
let v = fallible_func().unwrap_or(42);

// return the default value of the type on error
let v = fallible_func().unwrap_or_default();

// execute make_value(e) on error
let v = fallible_func().unwrap_or_else(make_value);

These are all fairly obvious ways you might want to deal with a error, and they are directly supported by Result.

Result supports many other methods, including map() which converts the successful result, leaving an error alone and map_err() that converts the error result, leaving the success value alone.

Conclusion

The Result<T, E> type in Rust makes error handling easier and more consistent than earlier error handling approaches. The default behavior of the Result<T, E> does not fail quietly and cannot easily be ignored. For a full discussion of Rust error handling, see the Rust doc.

Leave a Reply

Your email address will not be published. Required fields are marked *