Responses (0)
Result
s 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?
// what should be the return type?
fn integer_divide(a: u32, b: u32) -> _ {
a / b // can this operation fail?
}
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.
fn integer_divide(a: u32, b: u32) -> Result<u32, String> {
if b != 0 {
Ok(a / b)
} else {
Err("Division by zero!".into())
}
}
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:
using a default value or
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 Result
s 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.
integer_divide(6, 2) // => Ok(3), a Result containing the integer 3
integer_divide(6, 0) // => Err(...), a Result containing an string
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.
// This is JavaScript code
function integerDivide(a, b) {
if (b === 0) {
throw new Error("Division by zero!");
} else {
return a / b;
}
}
integerDivide(6, 2) // => 3, just the naked integer type
integerDivide(6, 0) // => returns nothing, it "throws" the error
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", Result
s 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.
enum Result<T, E> {
Ok(T),
Err(E),
}
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
) orfail 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:
let result = integer_divide(6, 2);
let doubled_result = result * 2;
// error: cannot multiply `{integer}` to `Result<u32, String>`
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:
let result = integer_divide(6, 2);
let doubled_result = result.unwrap() * 2;
println!("{}", doubled_result); // => prints 6
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:
let result = integer_divide(6, user_input_integer);
let treated_result = match result {
Ok(int) => int,
Err(_) => 0,
};
let doubled_result = treated_result * 2;
println!("{}", doubled_result); // => prints 6
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:
fn divide_then_double(input: u32) -> Result<u32, String> {
let result = integer_divide(6, input);
let treated_result = match result {
Ok(int) => int,
_ => return result,
};
Ok(treated_result * 2)
}
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.
fn divide_then_double(input: u32) -> Result<u32, String> {
let treated_result = integer_divide(6, input)?;
Ok(treated_result * 2)
}
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?
Read up on the examples on error handling might build your confidence that you know your results, panics, and exceptions
Beyond just error handling, these Rust exercises will help you learn and build confidence in your knowledge about Rust in general
Of course, you should take a look at Fullstack Rust