Rust Option type
last modified October 25, 2026
In this article we show how to work with the Option type in
Rust.
The Option type represents a value that may or may not be
present. Rust has no null references; a function that may have nothing to
return returns an Option instead. The type is an enum with two
variants:
enum Option<T> {
None,
Some(T),
}
Some(T) holds a value of type T, while
None signifies the absence of a value. An
Option<i32> is a different type from i32, so
the compiler does not let us use the value without first checking whether it is
there. This rules out the null pointer errors common in other languages. The
Option type and its variants are in the prelude; we do not need to
import them.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
Returning an Option
A function returns an Option when it may have no result. The
caller handles both cases with a match.
fn find_index(words: &[&str], target: &str) -> Option<usize> {
for (i, w) in words.iter().enumerate() {
if *w == target {
return Some(i);
}
}
None
}
fn main() {
let words = ["sky", "forest", "cup"];
for target in ["cup", "pen"] {
match find_index(&words, target) {
Some(idx) => println!("{target} is at index {idx}"),
None => println!("{target} not found"),
}
}
}
The find_index function looks for a word in a slice.
fn find_index(words: &[&str], target: &str) -> Option<usize> {
The function returns Some with the index of the word, or
None if the word is not in the slice. The standard library
provides the same functionality in the position method of
iterators.
match find_index(&words, target) {
Some(idx) => println!("{target} is at index {idx}"),
None => println!("{target} not found"),
}
The Some(idx) pattern extracts the value. The
match must handle the None case as well, otherwise
the code does not compile.
$ cargo run -q cup is at index 2 pen not found
Options in the standard library
Many methods of the standard library return an Option. In the
next example, we look up users in a hash map.
use std::collections::HashMap;
fn main() {
let users = HashMap::from([
("Alice", "administrator"),
("Bob", "developer"),
("Carol", "designer"),
]);
for name in ["Alice", "Eve", "Bob", "Dan"] {
match users.get(name) {
Some(role) => println!("{name}: {role}"),
None => println!("{name}: user not found"),
}
}
}
The get method of a HashMap returns
Some with a reference to the value, or None if the
key is missing.
let users = HashMap::from([
("Alice", "administrator"),
("Bob", "developer"),
("Carol", "designer"),
]);
We create a map from an array of key-value pairs.
match users.get(name) {
Some(role) => println!("{name}: {role}"),
None => println!("{name}: user not found"),
}
For each name, we print the role or a message that the user does not exist.
$ cargo run -q Alice: administrator Eve: user not found Bob: developer Dan: user not found
if let and let else
When only the Some case is interesting, if let is
shorter than a match. The let else statement
extracts the value or leaves the function early.
fn first_even(vals: &[i32]) -> Option<i32> {
vals.iter().copied().find(|v| v % 2 == 0)
}
fn describe(vals: &[i32]) {
let Some(v) = first_even(vals) else {
println!("{vals:?}: no even value");
return;
};
println!("{vals:?}: first even value is {v}");
}
fn main() {
if let Some(v) = first_even(&[1, 3, 8, 5]) {
println!("found {v}");
}
describe(&[7, 4, 2]);
describe(&[1, 3, 5]);
}
The first_even function returns the first even value of a slice,
if there is one.
if let Some(v) = first_even(&[1, 3, 8, 5]) {
println!("found {v}");
}
The block runs only if the function returns Some; the value is
bound to v.
let Some(v) = first_even(vals) else {
println!("{vals:?}: no even value");
return;
};
If the result is Some, the value is bound to v and
stays available for the rest of the function. Otherwise, the
else block runs and must leave the function.
$ cargo run -q found 8 [7, 4, 2]: first even value is 4 [1, 3, 5]: no even value
unwrap and expect
The unwrap method returns the value inside Some. If
the option is None, it panics. The expect method
does the same, but lets us provide the panic message.
fn main() {
let words = ["sky", "forest", "cup"];
let found = words.iter().position(|&w| w == "cup");
let missing = words.iter().position(|&w| w == "pen");
println!("cup is at index {}", found.unwrap());
println!("pen is at index {}", missing.expect("pen should be in the list"));
}
The first search succeeds and unwrap returns the index. The
second search returns None, and expect stops the
program with our message.
Use these methods only when None means a bug in the program, or
in short examples and tests. The Rust unwrap
article covers them in more detail.
$ cargo run -q cup is at index 2 thread 'main' (26997) panicked at src/main.rs:8:44: pen should be in the list note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Default values
The unwrap_or, unwrap_or_else, and
unwrap_or_default methods return the value inside
Some, or a fallback value for None.
use std::collections::HashMap;
fn main() {
let config = HashMap::from([("max_connections", 100), ("retry_attempts", 3)]);
let max_connections = config.get("max_connections").copied().unwrap_or(10);
let timeout = config.get("timeout").copied().unwrap_or(30);
println!("max connections: {max_connections}, timeout: {timeout}");
let port = config.get("port").copied().unwrap_or_else(|| {
println!("port not set, using the default");
8080
});
println!("port: {port}");
let retries: i32 = config.get("retries").copied().unwrap_or_default();
println!("retries: {retries}");
}
We read settings from a configuration map. If a setting is missing, a default value is used.
let timeout = config.get("timeout").copied().unwrap_or(30);
The get method returns an Option<&i32>;
the copied method turns it into an
Option<i32>. The unwrap_or method returns 30
since the key is missing.
let port = config.get("port").copied().unwrap_or_else(|| {
println!("port not set, using the default");
8080
});
The unwrap_or_else method takes a closure that computes the
fallback. The closure runs only for None, which matters when the
default is expensive to create or has side effects.
let retries: i32 = config.get("retries").copied().unwrap_or_default();
The unwrap_or_default method uses the default value of the type:
0 for numbers, an empty string for String.
$ cargo run -q max connections: 100, timeout: 30 port not set, using the default port: 8080 retries: 0
Transforming with map
The map method transforms the value inside Some and
leaves None unchanged.
fn main() {
let some_value = Some(5);
let none_value: Option<i32> = None;
println!("{:?}", some_value.map(|x| x * 2));
println!("{:?}", none_value.map(|x| x * 2));
let name: Option<&str> = Some("john doe");
let len = name.map(|n| n.len());
let upper = name.map(|n| n.to_uppercase());
println!("{len:?} {upper:?}");
println!("{}", none_value.map_or(String::from("n/a"), |x| x.to_string()));
}
We apply several closures to optional values.
println!("{:?}", some_value.map(|x| x * 2));
println!("{:?}", none_value.map(|x| x * 2));
The value 5 is doubled. For None, the closure is not called and
the result is None.
let len = name.map(|n| n.len()); let upper = name.map(|n| n.to_uppercase());
The closure can return a different type; an Option<&str>
becomes an Option<usize> or an
Option<String>.
println!("{}", none_value.map_or(String::from("n/a"), |x| x.to_string()));
The map_or method combines map and
unwrap_or: it returns the transformed value or the given
default.
$ cargo run -q
Some(10)
None
Some(8) Some("JOHN DOE")
n/a
Chaining with and_then
The and_then method chains operations that themselves return an
Option. With map, we would get a nested
Option<Option<T>>; and_then flattens it.
fn parse_first(text: &str) -> Option<i32> {
text.split_whitespace().next().and_then(|w| w.parse().ok())
}
fn main() {
println!("{:?}", parse_first("42 apples"));
println!("{:?}", parse_first("many apples"));
println!("{:?}", parse_first(""));
let half = |x: i32| if x % 2 == 0 { Some(x / 2) } else { None };
println!("{:?}", Some(20).and_then(half).and_then(half));
println!("{:?}", Some(20).and_then(half).and_then(half).and_then(half));
}
The parse_first function parses the first word of a text as a
number.
text.split_whitespace().next().and_then(|w| w.parse().ok())
The next method returns None for an empty text. If
there is a word, we try to parse it; the ok method converts the
Result of parse into an Option. The
whole chain returns None if any step fails.
let half = |x: i32| if x % 2 == 0 { Some(x / 2) } else { None };
println!("{:?}", Some(20).and_then(half).and_then(half));
The half closure halves even numbers and returns
None for odd ones. 20 is halved to 10 and then to 5; another
step fails, since 5 is odd.
$ cargo run -q Some(42) None None Some(5) None
filter and checks
The filter method keeps the value only if it satisfies a
condition. The is_some, is_none, and
is_some_and methods test an option without extracting the
value.
fn main() {
let ages = [Some(34), Some(15), None, Some(70)];
for age in ages {
let adult = age.filter(|&a| a >= 18);
println!("{age:?} -> {adult:?}");
}
let v = Some(4);
println!("{} {}", v.is_some(), v.is_none());
println!("{}", v.is_some_and(|x| x > 3));
}
We keep only the ages of adults.
let adult = age.filter(|&a| a >= 18);
For Some(15), the condition is false and the result is
None. A None stays None.
println!("{}", v.is_some_and(|x| x > 3));
The is_some_and method returns true if the option
holds a value that satisfies the condition.
$ cargo run -q Some(34) -> Some(34) Some(15) -> None None -> None Some(70) -> Some(70) true false true
The question mark operator
In a function that returns an Option, the ?
operator extracts the value from Some. If the option is
None, the function returns None immediately.
struct User {
name: String,
email: Option<String>,
}
fn email_domain(user: &User) -> Option<&str> {
let email = user.email.as_ref()?;
let at = email.find('@')?;
Some(&email[at + 1..])
}
fn main() {
let users = [
User { name: String::from("John"), email: Some(String::from("john@example.com")) },
User { name: String::from("Roger"), email: None },
User { name: String::from("Lucy"), email: Some(String::from("invalid")) },
];
for u in &users {
println!("{}: {:?}", u.name, email_domain(u));
}
}
The email_domain function returns the domain part of the user's
email address.
let email = user.email.as_ref()?;
let at = email.find('@')?;
The function returns None if the user has no email, or if the
email has no @ character. The as_ref method turns
the &Option<String> into an
Option<&String>, so we borrow the string instead of
moving it out of the struct.
Some(&email[at + 1..])
If both steps succeed, we return the text after the @
character.
$ cargo run -q
John: Some("example.com")
Roger: None
Lucy: None
Converting between Option and Result
The ok_or and ok_or_else methods turn an
Option into a Result, giving the missing value an
error. The ok method of Result goes the other
way.
fn get_item(items: &[&str], idx: usize) -> Result<String, String> {
let item = items.get(idx).ok_or_else(|| format!("no item at index {idx}"))?;
Ok(item.to_uppercase())
}
fn main() {
let items = ["pen", "book", "cup"];
println!("{:?}", get_item(&items, 1));
println!("{:?}", get_item(&items, 5));
let r: Result<i32, String> = Err(String::from("failed"));
println!("{:?}", r.ok());
}
The get_item function returns an error message when the index is
out of range.
let item = items.get(idx).ok_or_else(|| format!("no item at index {idx}"))?;
The get method returns an Option. We turn
None into an Err with a message and propagate it
with ?. We use ok_or_else rather than
ok_or, so the message is built only when it is needed.
println!("{:?}", r.ok());
The ok method converts a Result into an
Option, discarding the error.
$ cargo run -q
Ok("BOOK")
Err("no item at index 5")
None
Source
std::option module documentation
In this article we have worked with the Option type in Rust.
Author
List all Rust tutorials.