VOOZH about

URL: https://blog.logrocket.com/understanding-rust-option-results-enums/

⇱ Understanding Rust Option and Result enums - LogRocket Blog


2022-04-01
1675
#rust
Greg Stoll
101499
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 Understanding Rust Option Results Enums

One of these conveniences is using enums, specifically the Option and Result types. So, in this post we’ll cover the following:

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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.

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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

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!


More great articles from LogRocket:


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 web frontends for Rust apps

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 lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.

LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.

👁 LogRocket Dashboard Free Trial Banner

Modernize how you debug your Rust apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

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 now