Greg Stoll Greg is a software engineer with over 20 years of experience in the industry. He enjoys working on projects in his spare time and enjoys writing about them!

Understanding Rust Option and Result enums

5 min read 1675

Understanding Rust Option Results Enums

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:

The Option type

Rust’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
  • or 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!

Using 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.

We made a custom demo for .
No really. Click here to check it out.

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).

Using 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.");
};

Using 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!

Using 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.

Using 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.

The Result type

Rust’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!

Using 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.

Using 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!

Using the ? operator

Ok, 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.

Using 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!

Using 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());
}

Conclusion

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.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking 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 app. 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 — .

Greg Stoll Greg is a software engineer with over 20 years of experience in the industry. He enjoys working on projects in his spare time and enjoys writing about them!

Leave a Reply