Introduction to Results in Rust

Responses (0)


Results are a fundamental concept to grasp to become a proficient Rust developer, but they can feel a little weird coming from JavaScript. But if we take it slow, they're not that hard to understand: Result is just a way to handle errors with a type.

Below, I'm going to guide you through the ins and outs of the Result concept in the Rust programming language.

What will you learn#

  • The problem Results solve and how

  • Where to use Results in practice

  • How to understand the Result type definition

  • How to use the values contained in a Result, safely or otherwise

By the way, if you're just getting started with Rust, and you want to learn to build Rust applications checkout the Fullstack Rust Book

Coming from JavaScript, one of the first stumps beginner Rust developers hit is on error handling. Rust statically types errors and treats them as any other data type, consolidating most of the error treatment code around the Result type. This is a lot different than exception-based approaches, so it's worth a closer look.

Okay, so what is a Result?#

A Result is a data type that is expected to be returned from functions that can fail in an expected and recoverable way.

Results are the output of fallible operations - you might also find result's module documentation helpful as you read this blog

So put another way, a Result is "just" the return type of a function that is not guaranteed to work.

But what kind of functions are not guaranteed to work?

Imagine a function that divides two integers. If 6 and 2 are parameters, then the answer will be 3. We might be inclined to think our function's return type is an integer as well, but hold up. Can we guarantee that a division between two integers will always work?

Well, let's say we have 6 and 0 as inputs. What should be the answer in this case? What integer would be a valid answer in this case? That's right, there is none; Because 6 divided by 0 is not an integer - here it's a failure. A perfect case for the Result type.

Because our integer division function won't return an integer, we need to return something else. In this case, we'll use a Result to wrap the integer when there is a success, or the Result will wrap the explanation of why an integer could not be generated.

Because this function is fallible it returns a Result so that the user can deal with its failure properly.

💡 Tip: when using Result, failures must be expected and well defined

This is also an important part of how we use Result: errors that cannot be anticipated, cannot be handled properly upstream.

If we want to properly define and type this function, we must know beforehand that it can fail. We must know 6 divided by 0 cannot output an integer and plan for that if we intend to always output the correct data type.

An unexpected failure cannot be annotated (because we don't know what it is yet) and it's handling will be impossible. However, an expected failure can have it's failure "encoded" as a Result which will allow us to properly handle the failure case.

💡 Tip: Failures must be recoverable and will be handled by the caller

Result formally tells the caller of a function that the function call can "fail" -- this means the caller is not surprised when the call fails, and we have alternative code paths for when it happens. Common alternative paths involve:

  1. using a default value or

  2. passing up the error.

One feature of the Rust compiler is that it will require Result to be handled. There are several ways you can do this, if you don't provide and handling code it will cause the compiler to raise a warning. This feature is great because we can be fearless about the execution of our programs -- we know that when the compilation returns no warning flags for our code, we've handled all of the Result possibilities.

Result vs Exceptions#

You might think that the definition of Result resembles JavaScript Errors or Ruby Exceptions. Exceptions on these languages have similar intentions as our Results here, but there are two crucial differences to note.

1 . Exceptions represent the error, while Results represent the whole outcome

Let's think back to our integer division example. The function returns a Result so it can account for the outcome when there is a division by 0. However, if the division works properly the returned data type is still a Result. The Result is returned either way because it contains the operation outcome as a whole.

Now, let's think about Exceptions. We usually see them only when something went wrong. The successful path just returns the naked data type (the integer in our case). In other words, the function would yield the exception or the successful data type.

Kind of weird, right?

Result gives us consistency because we always get a Result regardless of success or failure.

2 . Exceptions are special, while Results are just a regular type

When talking about Exceptions above, you might have taken note of the "yield" verb. Why couldn't I have just used the "return" verb? An Exception is a special kind of data type on languages that implement it. They are generally seen together with verbs like "raise" and "throw", which are reserved keywords used to notify the runtime that something bad happened.

Getting more technical, we could say an Exception is thrown, which will reverse the execution stack until it's properly dealt with. This means Exceptions are special to the runtime and have special keywords defined so they can be interacted with.

Results, on the other hand, are just a data type that you return from a function. The norm is to treat them like you would treat any other data type. This lowers the cognitive load.

Also, because Result is "just a regular type", Results are part of the type signature for every function they are used in: so you'll never be surprised when a Result is returned. The compiler won't let you be surprised: it will tell you beforehand and make sure you handle it.

Anatomy of a Result#

At this point, I hope you understand the basic rationale behind Result. But let's figure out how to read it and use it in more practical situations.

The Result in Rust is defined as an enumeration of two options, with two generic types.

Aside for polyglots: One curious bit about this definition is that it's pretty common in many languages that have a Result implementation, like Elm and Swift. Many concepts read here transfer cleanly to those contexts.

Here is the definition directly from Rust's code.

The two generic types, T and E, represent the type of the successful and failed outcomes respectively. These values will each be contained by the Result. Each constructor carries one of these types, so we have a constructor for successes (Ok) and another for failures (Err).

Going back to our example, our integer division example returns a Result<u32, String>, which means the operation can either:

  • be successful and return an integer (u32) or

  • fail and return a String.

The construction of a "success", would be the number wrapped by an Ok constructor (6 / 2 = Ok(3)). The failure, on the other hand, would be wrapped by an Err constructor (6 / 0 = Err("Division by zero!")).

Result unwrapping#

Say that after we perform the integer division we want to do another operation with the outcome. Let's say we want to divide 6 by 2, then double the result.

We could naively try to multiply the Result by 2, which would promptly yield a compiler error:

The error you see is the compiler looking out for us. It letting us know that we are assuming an operation-that-can-fail will never fail.

In other words, we are trying to multiply the successful outcome by 2, but we aren't handling the case where the division has failed.

So how should the program react in the case where our division fails?

Well, in this case, we know the division can never fail since the inputs are the constants 6 and 2, which we know will result in Ok(3). In these cases, we can tell Rust we are super-sure-and-positive the error will never happen, so just trust us:

The unwrap function does this: it removes the Result from the equation.

Of course, you're probably wondering, what happens if the outcome was a failure? In our case, the error would be associated with a String type, which cannot be multiplied by two.

That's the risk you take when you unwrap a Result.

You, as a developer, are taking the responsibility and vowing the Result will never be a failure. If it is, Rust will crash your whole program with a panic. At that point, nothing can stop the inevitable crash.

Scary, huh? So what should we do if we are not sure the Result is always a success? Or if you are simply not comfortable taking this kind of risk? For that, we'll use Result matching.

Result matching#

Unwrapping a Result is risky. Unwrapping is only safe if there is no way for the error to happen, which is unusual in complex systems.

In real-world programs, the arguments often come from the outside (like user input) - we just can't be sure that the division will work or not. For these cases, we would like to know if the operation succeeded or failed without risking a crash.

Rust has a powerful pattern matching implementation that works perfectly for us in this case.

For example, we could use it to default an error to a constant value (like 0), for example:

The match keyword lets us test against Result constructors and associate a path of execution to each. And this example above shows how to provide a default value for a failure.

But as we said before, we could also pass the error "up the chain". Let's see what it looks like:

Now we are passing the problem to the caller, just like the integer_divide function does. If the division fails, we don't know what to do, so we just return the Result. Note that in this case, we have to wrap the successful result in an Ok constructor so function signature is respected.

The ? operator#

This pattern of unwrapping or returning the result is so prevalent and useful, we Rust developers got tired of writing it every time. Rust has a macro that does just that for us.

This looks better, doesn't it? The star of the show here is the ? operator. The funny thing about this example is that both the match and the ? are the same from the compiler's point of view. The macro and operator just make it easier to read.

So where to go from here?