A gentle introduction to Results in Rust

This article will guide you through the ins and outs of the Result concept in the Rust programming language. Results are prevalent in Rust standard library and are a fundamental concept to grasp to become a prominent Rust developer.

Responses (0)


Clap
0|0|

Here is how it's gonna go.

  • We'll discuss the definition and why the concept exists

  • Then we're gonna compare it to competing concepts in popular languages

  • Lastly, we'll figure out how to use it in practice

Introduction#

Rust is a system programming language launched back in 2010 that has been gaining a lot of traction in the last few years. If the Stack Overflow survey is indicative of anything, is that more and more people are trying and loving it as their next pet programming language. In contrast, Rust is known to be a difficult language to learn and challenges even the most senior developers with their low-level constructs and new concepts.

Coming from more popular languages like Ruby or 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. The concepts and treatments it gets are inherently different from most exception-based approaches, but there are also many parallels we can make. Let's walk through these similarities and differences!

What you need to know#

This article assumes no knowledge of the Result abstraction, which is precisely what we intend to cover next. It assumes general familiarity with the Exceptions error handling abstraction, but in case you haven't heard of it, you can still benefit from this article and maybe discover a couple ways of dealing with errors in your software.

From the Rust language, some knowledge about the syntax is advised, but nothing further. So if you are familiar with simple Rust examples, you're good to go.

If you're just getting started with Rust, you may want to check out some of the amazing introductory content out there. If you want a more technical and to the point read, you might want to check out the Official Rust Book. For a more structured and detailed course, the Fullstack Rust Book will hit all the right spots.

What will you learn#

  • The problem Results want to solve and how

  • Where to expect to use and find Results in practice

  • How to understand the Result type definition

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

What is a Result?#

According to the result's module documentation, a Result is a data type that is expected to be returned from functions that can fail in an expected and recoverable way. This is a short but insightful description of the usability and habitat of the Result, which points to the three main aspects to note about the Result abstraction.

Results are the output of fallible operations

That's all they are, the return type of a function that is not guaranteed to work. So what is a function that is not guaranteed to work?

Imagine you are making a function that divides two integers. If you get 6 and 2 as parameters, you know the answer should be 3. From this simple example you might be inclined to think your function's return type is an integer as well, but hold up. Can you guarantee that a division between two integers will always work?

You probably already know where I'm getting at, but let's say you have a 6 and a 0 as inputs. What should be the answer in this case? More provocatively, what would be the integer that can work as the answer in this case? That's right, there is none because 6 divided by 0 is not an integer, it's a failure.

So our integer division function wouldn't return an integer, but a Result that would wrap the integer, in case of success, or the explanation of why an integer could not be generated. The function is fallible, and therefore, returns a Result so the user can deal with its failure properly.

Failures must be expected and well defined

Which leads us here. If we want to properly defined and type this function, we must know beforehand it can fail. We must know 6 divided by 0 cannot output an integer, so we can check against it and output the correct data type.

This is also an important part of the Result usage: errors that cannot be anticipated, cannot be treated properly up the chain. An expected failure of an operation is part of its signature and will be properly annotated with a Result. Conversely, an unexpected failure cannot be annotated (because we don't know what it is yet) and it's handling will be impossible.

Failures must be recoverable and will be handled by the caller

Which finally leads here. Results are formally telling the caller of a function that it can fail so the caller is not surprised when it does. Not being surprised means having paths inside the program for when it happens. Common alternative paths involve using a default value or passing up the error, as we'll see in the next sections.

One feature Results in Rust get, which makes them even more reliable, is that the compiler requires them to be treated. There are several ways they can be handled, but providing no handling code explicit will cause the compiler to raise a warning. I love this feature because it makes me fearless about the execution of my program when the compilation returns no warning flags for my code.

Result vs Exception#

If you're coming from languages like Ruby or JavaScript you might be thinking the definition we talked about earlier resembles Errors or Exceptions. If you made this connection, we are on track! Exceptions on these languages are abstraction created with very similar intentions as our Results here, but there are two crucial differences to note.

Exceptions represent the error, while Results represent the whole outcome

Let's think back to our integer division example for a minute. The function returns a Result so it can account for the outcome in case of a division by zero. In case the division works properly the returned data type is still a result, representing the successful outcome. 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 when something went wrong and only then. The successful path would return just the naked data type, the integer in our case. In other words, the function would yield the exception or the successful data type, differently from the Result, which is always returned containing the error or successful data type.

Exceptions are special, while Results are just a regular type

You might have taken note of the "yield" verb in the last sentence. 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, reversing 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 some data type you return from a function. Treating them like you would treat any other data type is the norm, which makes the cognitive load on the developers that use it lower.

Further, being just a regular type, Results are part of every signature they are used in, so you'll never be surprised when it 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#

I hope from this point we are all on board of the Result train. Let's figure out how to read it and use it in more practical scenarios then, shall we?

The Result in Rust is defined as an enumeration of two options, with two generic types. 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 common directly from Rust's code.

The two generic types, T and E, represent the type of the successful and failed outcomes respectively, to be contained by the Result. Each constructor carries one of these types, so we have a constructor for successes (Ok) and another from failures (Err). That's all there is to it.

Getting more practical, our integer division example returns a Result<u32, String>, which means the operation can either be successful and return an integer or fail and return a string. The construction of a Success, as we have already seen, would be the successful 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#

Following the integer division, you might 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 just the compiler looking out for you. It is trying to let you know that you are assuming the operation that can fail will never fail. In other words, you are trying to multiply the successful outcome by 2, but what if the division had failed? How should the program react in case the division fails?

Rust wants you to deal with it, so let's do it. 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 sure and positive the error will never happen.

The unwrap function does exactly that: it removes the Result from the equation. You might be 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. Further, even if it could, it might not make sense for a developer that is excepting the resulting division to be there.

That's the danger and 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 this point, nothing can stop the inevitable crash and burn.

Scary, huh? So what to do if you are not sure the result is always positive, or if you just are not comfortable taking on this kind of risk? (I am always on the latter category).

Result matching#

From the previous section, you might have gotten a feeling that unwrapping a Result is risky. Unwrapping is only safe if there is no way for the error to happen, which is unusual in more complex systems. In real-world programs, the inputs could be from user input, for example, which implies that we could not be sure if the division would work or not. For these cases, we would like to know if the operation succeeded or failed without risking the crash of the program.

Rust has a powerful pattern matching implementation that works perfectly for us in this case. We could, for example, use it to default an error to a constant value, like zero for example.

The match keyword lets us test against Result constructors and associate a path of execution to each. What this means is that we could get the successful integer, in case it succeeds, as long as we tell the program what to do in case it fails.

The previous example 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 on the eyes for us developers.

Conclusion#

Ok, things might have gotten a little intense there at the end. If you are here with me until this point, I know we already know your Result. Let's look back at what we've learned.

  • The problem Results want to solve and how

  • Where to expect to use and find Results in practice

  • How to understand the Result type definition

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

If you enjoyed learning about this abstraction, you might want to read up on some of the other cool ones Rust has to offer. One of the best things about learning a new programming language is how it encourages you to look at problems another way. So where to go from here?

Don't fear the Result and be safe out there, fellow rustaceans!

Clap
0|0