ZetCode

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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
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

Option enum documentation

In this article we have worked with the Option type in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.