Rust Error Handling, Part One

By | January 16, 2022

For the last couple of years, I’ve been learning Rust in my free time (ha!) by working on side projects. Since I don’t really have a deadline, I can focus on seeing how Rust changes the way I think about programming.

A language that doesn’t affect the way you think about programming is not worth knowing.

Alan Perlis

As I’m using the language (and comparing it to others I’ve used in the last few decades), I’m finding particular design decisions that I really like. In this post, I’m going to try to explain what I like about the first.

Error Handling Approaches

In the past, I’ve seen 3 basic approaches to handling errors.

  • Ignore them/crash
  • Error return values
  • Exceptions

Ignoring Errors

Most new programmers start with the first strategy. They may not even think about the possibility of errors. This approach basically only codes for the happy path and relies on hope that no one will give the program bad input and that the programmer made no mistakes.

If you are building a quick-and-dirty utility that only you will ever use, this might not be a bad strategy.

Some users of the Elixir programming language or fans of managed systems also claim this is a viable strategy. Some monitor will recover from the problem and restart the program.

Error Returns

The second strategy is used by most imperative/procedural languages. Any routine or operation that might fail returns an error code that allows the calling code to know if there is an error. One advantage of this approach is that error handling is pretty straight-forward. You always test for an error everywhere one can occur. You determine an appropriate approach for handling each error value and then you continue or quit as appropriate.

If necessary, you can return the error code to the calling function, so that it can handle the error (possibly transforming the value if needed).

This approach has one glaring disadvantage, though. If you forget to check an error return, the code will continue as if nothing was wrong. This often results in the code eventually crashing in a way that makes little sense, far from the point of the actual error.

Worse, the case of a missing error check looks just like code that is written correctly, but cannot return an error. This means that getting error handling right requires that the programmer does a perfect job of checking all error returns. History shows that rarely happens.

Exceptions

The next approach to error handling involved exceptions. An exception is thrown/raised for different kinds of errors and happens independently of return values from a function. The default behavior for an exception was to unwind the call stack until it is caught. If the exception is never caught, it terminates the program.

Unlike error return values, exceptions default to termination. So, this solved the error return problem of errors being missed be accident. Unfortunately, it has the opposite issue that even minor errors need to be caught or the code terminates.

Comparison

Comparing these three approaches gives the following trade-offs.

ApproachHandle ErrorDefault BehaviorFailure Mode
IgnoreN/AContinueUnknown
Error ReturnCheck return and branchContinueInvalid values used;
Failure later
ExceptionCatch exceptionTerminate programTerminate even
if recoverable error

All of these approaches have two parts to their failure mode: ignored error has unexpected behavior, and it’s hard to spot if you accidentally left off the error handling. Exceptions at least terminate the program if there is a problem, but you can’t just look at the code to determine where you forgot to handle the error.

Rust’s Result enum

Rust’s error handling is based around the type Result<T, E>. This type resolves to either one of two variants: one contains a return value of the type specified by type T, the other is an error of the type specified by type E. The only way to retrieve the appropriate data is through pattern matching or one of the methods of Result<T, E>. This makes it slightly safer than a simple error return, because you can’t accidentally use the error value as the actual value.

More importantly, the Result type is marked with the must_use attribute. That means if a function returns a Result, the program is required to handle that value in some way. You can pattern match, or assign the value to variable, but you can’t just ignore it.

Next Time

Next time, we will explore the different ways you can use a Result. This will help you see why Rust’s error handling may be superior to traditional error returns or exceptions.

Leave a Reply

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