Skip to content
AgileLogics
5 min read

Option in Depth

Prerequisites: Cargo, dependencies & workspaces

Level 1 introduced Option<T>: a value that is either Some(value) or None. Matching on it every time gets repetitive, so Option has methods for the common cases. Most of them take a closure — a small inline function written |x| x + 1. Closures get their own lesson later; for now read |x| expr as "given x, produce expr".

Syntax

fn main() {
    let some: Option<i32> = Some(4);
    let none: Option<i32> = None;

    let _ = some.map(|n| n * 2);                    // Some(8): transform the inner value
    let _ = some.and_then(|n| n.checked_div(0));    // None: the closure itself returns an Option
    let _ = some.filter(|n| *n > 3);                // Some(4): keep only if the test passes
    let _ = none.unwrap_or(0);                      // 0: fallback value
    let _ = none.unwrap_or_else(|| 2 + 2);          // 4: fallback computed only when needed
    let _ = none.unwrap_or_default();               // 0: the type's Default value
    let _ = none.ok_or("missing");                  // Err("missing"): convert to a Result
    let _ = some.is_some();                         // true
}

map is for closures that return a plain value; and_then is for closures that return another Option, and flattens the result so you don't end up with Option<Option<T>>.

The ? operator on Option

Inside a function that returns Option, writing ? after an Option means "if this is None, return None from the whole function right now; otherwise give me the inner value":

fn first_even_squared(numbers: &[i32]) -> Option<i32> {
    let first = numbers.first()?;   // returns None early for an empty slice
    if first % 2 != 0 {
        return None;
    }
    Some(first * first)
}

Example

struct User {
    name: String,
    nickname: Option<String>,
}

fn parse_age(input: &str) -> Option<u32> {
    input.trim().parse().ok()   // .ok() turns a Result into an Option
}

fn nickname_initial(user: &User) -> Option<char> {
    let nick = user.nickname.as_ref()?;   // as_ref: borrow the String instead of moving it
    nick.chars().next()
}

fn main() {
    println!("{:?}", parse_age(" 41 ").map(|age| age + 1));
    println!("{}", parse_age("forty").unwrap_or(0));

    let even_doubled = Some(8).filter(|n| n % 2 == 0).map(|n| n * 2);
    println!("{even_doubled:?}");

    let ada = User { name: String::from("Ada"), nickname: Some(String::from("countess")) };
    let bob = User { name: String::from("Bob"), nickname: None };

    for user in [&ada, &bob] {
        let initial = nickname_initial(user)
            .map(|c| c.to_ascii_uppercase())
            .unwrap_or('-');
        println!("{}: {}", user.name, initial);
    }

    let checked: Result<u32, String> = parse_age("x").ok_or(String::from("not a number"));
    println!("{checked:?}");

    let label = bob.nickname.as_deref().unwrap_or("(none)");
    println!("Bob's nickname: {label}");
}

Expected output

Some(42)
0
Some(16)
Ada: C
Bob: -
Err("not a number")
Bob's nickname: (none)

as_deref() turns an &Option<String> into an Option<&str>, which is handy for supplying a &str default without allocating a new String.

Common mistakes

  • Calling .unwrap() on a None. It compiles but panics at runtime with called `Option::unwrap()` on a `None` value. Use unwrap_or, ?, or a match unless None is truly impossible.
  • Using ? in a function that returns (), such as main. The compiler says the `?` operator can only be used in a function that returns `Result` or `Option` — change the return type or handle the None explicitly.
  • Writing opt.unwrap_or(expensive()). The argument is evaluated even when opt is Some. Use unwrap_or_else(|| expensive()) so the work only happens when it is needed.
  • Using map with a closure that returns an Option and getting Option<Option<T>>. Use and_then instead.
  • Moving a String out of a struct with user.nickname? when you only have &User: error[E0507]: cannot move out of `user.nickname` which is behind a shared reference. Borrow it with .as_ref() first.

Practice

Write fn middle_char(word: &str) -> Option<char> that returns the middle character of a word with an odd number of characters and None otherwise (including for an empty string). Use it to print Some('s') for "rusty" and None for "rust".

Show solution
fn middle_char(word: &str) -> Option<char> {
    let len = word.chars().count();
    if len % 2 == 0 {
        return None;
    }
    word.chars().nth(len / 2)
}

fn main() {
    println!("{:?}", middle_char("rusty"));
    println!("{:?}", middle_char("rust"));
}