Rust is a systems programming language that focuses on safety and performance, and has been voted the “most loved language” on Stack Overflow’s annual survey for six years running! One of the reasons Rust is such a joy to program in is that, despite its focus on performance, it has a lot of well-thought-out conveniences that are frequently associated with higher-level languages.
One of these conveniences is using enums, specifically the Option
and Result
types. So, in this post we’ll cover the following:
Option
typeRust’s version of a nullable type is the Option<T>
type. It’s an enumerated type (also known as algebraic data types in some other languages) where every instance is either:
None
Some(value)
This is where value
can be any value of type T
. For example, Vec<T>
is Rust’s type that represents a vector (or variable-sized array). It has a pop()
method that returns an Option<T>
, which will be None
if the vector is empty or Some(value)
containing the last value of the vector.
One of the benefits of an API that returns an Option
is that to get the value inside, callers are forced to check if the value is None
or not. This avoids problems in other languages that don’t have nullable types.
For example, in C++, std::find()
returns an iterator, but you must remember to check it to make sure it isn’t the container’s end()
—if you forget this check and try to get the item out of the container, you get undefined behavior.
The downside is that this tends to make code irritatingly verbose. But, Rust has a lot of tricks up its sleeve to help!
expect
and unwrap
If you’re sure that an Option
has a real value inside, then expect()
and unwrap()
are for you! They return the value inside, but if the variable is actually None
, your program exits. (This is known as panicking, and there are cases when it is recoverable, but for simplicity, we’ll gloss over that here.)
The only difference is that expect()
lets you specify a custom message that prints out to the console as the program exits.
There’s also an unwrap_or()
, which lets you specify a default if the value is None
, so Some(5).unwrap_or(7)
is 5
and None.unwrap_or(7)
is 7
.
If you want, you can check whether the Option<T>
has a value before calling unwrap()
like this:
// t is an Option<T> if t.is_some() { let real_value = t.unwrap(); }
But, there are more concise ways to do this (for instance, using if let
, which we’ll cover later).
match
The most basic way to see whether an Option
has a value or not is to use pattern matching with a match
expression. This works on any enumerated type, and looks like this:
// t is an Option<T> match t { None => println!("No value here!"), // one match arm Some(x) => println!("Got value {}", x) // the other match arm };
One thing to note is that the Rust compiler enforces that a match
expression must be exhaustive; that is, every possible value must be covered by a match arm. So, the following code won’t compile:
// t is an Option<T> match t { Some(x) => println!("Got value {}", x) };
And we get an error:
error[E0004]: non-exhaustive patterns: `None` not covered
This is actually very helpful to avoid times when you think you’re covering all the cases but aren’t! If you explicitly want to ignore all other cases, you can use the _
match
expression:
// t is an Option<T> match t { Some(x) => println!("Got value {}", x), // the other match arm _ => println!("OK not handling this case."); };
if let
It’s pretty common to want to do something only if an Option
has a real value, and if let
is a concise way to combine doing that with getting the underlying value.
For instance, the following code will print "Got <value>"
if t
has a value, and do nothing if t
is None
:
// t is an Option<T> if let Some(i) = t { println!("Got {}", i); }
if let
actually works with any enumerated type!
map
There are also a bunch of ways to do things to an Option<T>
without checking whether it has a value or not. As an example, you can use map()
to transform the real value if it has one, and otherwise leave it as None
.
So, for example, Some(10).map(|i| i + 1)
is Some(11)
and None.map(|i| i + 1)
is still None
.
into_iter
with Option
If you have a Vec<Option<T>>
, you can transform this into an Option<Vec<T>>
, which will be None
if any of the entries in the original vector were None
.
This makes sense if you think about receiving results from many operations and you want the overall result to fail if any of the individual operations failed.
So, for example vec![Some(10), Some(20)].into_iter().collect()
is Some([10, 20])
while vec![Some(10), Some(20), None].into_iter().collect()
is None
.
Result
typeRust’s Result<T, E>
type is a convenient way of returning either a value or an error. Like the Option
type, it’s an enumerated type with two possible variants:
Ok(T)
, meaning the operation succeeded with value T
Err(E)
, meaning the operation failed with an error E
It’s very convenient to know that if a function returns an error, it will be this type, and there are a bunch of helpful ways to use them!
ok_or
Since Option
and Result
are so similar, there’s an easy way to go between the two. Option
has the ok_or()
method: Some(10).ok_or("uh-oh")
is Ok(10)
and None.ok_or("uh-oh")
is Err("uh-oh")
.
Then, Result
has the ok()
method: Ok(10).ok()
is Some(10)
and Err("uh-oh").ok()
is None
.
There’s also an err()
method on Result
that does the opposite: errors get mapped to Some
and success values get mapped to None
.
expect
, unwrap
, match
, and if let
Just like with Option
, if you’re sure a Result
is a success (and you don’t mind exiting if you’re wrong!), expect()
and unwrap()
work exactly the same way as they do for Option
.
And, since Result
is an enumerated type, match
and if let
work in the same way, too!
?
operatorOk, this is where things get really cool. Let’s say you’re writing a function that returns a Result
because it could fail, and you’re calling another function that returns a Result
because it could fail.
Many times if the other function returns an error, you want to return that error straight out of the function. So, your code would look like the following:
let inner_result = other_function(); if let Err(some_error) = inner_result { return inner_result; } let real_result = inner_result().unwrap(); // now real_result has the actual value we care about, we can continue on...
But, this is kind of a pain to write over and over. Instead, you can write this code:
let real_result = other_function()?;
That’s right: the single ?
operator does all of that! What’s even better is that you can chain calls together, like so:
let real_result = this_might_fail()?.also_might_fail()?.this_one_might_fail_too()?;
Another common technique is to use something like map_err()
to transform the error into something that makes more sense for the outer function to return, then use the ?
operator.
must_use
The Rust compiler is notoriously helpful, and one of the ways it helps is by warning you about mistakes you might be making.
The Result
type is tagged with the must_use
attribute, which means that if a function returns a Result
, the caller must not ignore the value, or the compiler will issue a warning.
This is mostly a problem with functions that don’t have a real value to return, like I/O functions; many of them return types like Result<(), Err>
(()
is known as the unit type), and in this case, it’s easy to forget to check the error since there’s no success value to get.
But, the compiler is there to help you remember!
into_iter
with Result
Similar to Option
, if you have a Vec<Result<T, E>>
you can use into_iter()
and collect()
to transform this into a Result<Vec<T>, E>
, which will either contain all the success values or the first error encountered.
So, for example, the following is Ok([10, 20])
:
vec![Ok(10), Ok(20)].into_iter().collect()
Then, this is Err("bad")
:
vec![Ok(10), Err("bad"), Ok(20), Err("also bad")].into_iter().collect()
If you want to gather all the errors instead of just the first one, it’s a little trickier, but you can use the handy partition()
method to split the successes from the errors:
let v: Vec<Result<_, _>> = some_other_function(); let (successes, errors): (Vec<_>, Vec<_>) = v.into_iter().partition(Result::is_ok); if !errors.is_empty() { return Err(errors.into_iter().map(Result::unwrap_err).collect()); } else { return Ok(successes.into_iter().map(Result::unwrap).collect()); }
The ideas behind Option
and Result
are not new to Rust. What stands out for me is how easy the language makes it to do the right thing by checking errors, especially with the ?
operator.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]