Option and its combinators

In Rust Option<T> is the enum for a value that may be absent: Some(value) or None. There is no null in Rust, so a function that may fail to find something says so in its return type, and the caller has to handle both variants before using the value.

Coming from C++? Null and Option looks a key up with std::optional and with Option, and then dereferences the empty one in both languages: one compiles and reads garbage, the other does not compile.

This lesson covers the enum, the combinators that keep the handling short, the two control-flow forms and the question mark. The last section is the error for treating an Option as a plain value, which is the mistake the type is designed to prevent.

Some and None

values.get(1) returns an Option because the index may be out of bounds. A match on it has two arms, and the compiler checks that both are there, so the absent case cannot be forgotten.

The program looks up an index that exists and prints the value it found. The None arm is not taken, but it is still written, which is the point of the type.

fn main() { let values = [10, 20, 30]; let found = values.get(1); match found { Some(value) => println!("found {value}"), None => println!("nothing"), } }

rustc --edition 2024 some.rs
./some

found 20

The same enum is the type of find on a string, of chars().next() and of every lookup that may miss. The lesson on enums explains why a standard library type can be matched like this.

unwrap_or, map and and_then

A match is clear but long when the value is only transformed. unwrap_or supplies a default, map transforms the value inside and leaves None alone, and and_then chains a function that itself returns an Option.

The program applies all three to a value and to the empty one. The debug output shows Some(10) after the map and Some(5) after the closure that rejects small values.

fn main() { let some: Option<i32> = Some(5); let none: Option<i32> = None; println!("{}", some.unwrap_or(0)); println!("{}", none.unwrap_or(0)); println!("{:?}", some.map(|value| value * 2)); println!("{:?}", some.and_then(|value| if value > 3 { Some(value) } else { None })); }

rustc --edition 2024 combinators.rs
./combinators

5
0
Some(10)
Some(5)

The closures in the calls are the subject of closures, and the pattern is the same as the iterator chains in iterator adaptors. Neither creates an allocation.

if let

When only the Some case needs code, if let Some(name) = value binds the payload and runs the block. An else branch may follow, which is the shortest form of a two-arm match.

The program greets a present name and reports a missing one. The second call takes the else branch, and the first prints the value it bound.

fn main() { let name: Option<&str> = Some("Rust"); if let Some(name) = name { println!("hello {name}"); } let missing: Option<&str> = None; if let Some(name) = missing { println!("hello {name}"); } else { println!("no name"); } }

rustc --edition 2024 iflet.rs
./iflet

hello Rust
no name

A chain of if let expressions is a match written as conditions, and the lesson on patterns shows when the match is the better shape.

The question mark on Option

Inside a function that returns Option, the question mark unwraps a value or returns None from the whole function. It is the same operator as the one on Result, which the next lesson covers, and it saves a match at every step.

The program takes the first character of a string and returns None for an empty one. The first call prints Some('r'), the second prints None.

fn first_char(text: &str) -> Option<char> { let first = text.chars().next()?; Some(first) } fn main() { println!("{:?}", first_char("rust")); println!("{:?}", first_char("")); }

rustc --edition 2024 question.rs
./question

Some('r')
None

The function signature must say Option in its return type, or the question mark is an error. The next lesson makes the same point for errors, where the value in the Err variant carries the reason.

An Option is not a number

An arithmetic operator does not look inside an Option. Writing Some(3) + 1 is E0369, and the message shows both operand types and points at the operator, which is the compiler refusing to guess what the absent case should mean.

The program below adds one to an Option. The fix is unwrap_or(0) + 1, or a match, and the choice between them is a decision about the missing value.

fn main() { let total = Some(3) + 1; println!("{total}"); }

rustc --edition 2024 add_err.rs

error[E0369]: cannot add `{integer}` to `Option<{integer}>`
--> add_err.rs:2:25
|
2 | let total = Some(3) + 1;
| ------- ^ - {integer}
| |
| Option<{integer}>
|
note: `Option<{integer}>` does not implement `Add<{integer}>`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/core/src/option.rs:600:0
|
= note: `Option<{integer}>` is defined in another crate

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0369`.

The same message appears for every operator that has no implementation for Option, which is most of them. The lesson on operator traits explains where the missing implementation would have to come from.

Exercises

  1. Write a function that returns the first word of a string as an Option and prints None for an empty one.
  2. Chain map and and_then to turn a string into its length, doubling it only when the length is above three.
  3. Replace the if let in the third program with a match and compare the two versions.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What are the two variants of Option?

    Answer

    Some and None

    Some carries a value and None means it is absent.

  2. What does unwrap_or(0) do on an Option?

    Answer

    Returns the value, or zero when it is None

    unwrap_or supplies the fallback value instead of panicking.

  3. What does map do on an Option?

    Answer

    Transforms the value inside and leaves None alone

    map applies the closure only to Some and keeps the shape of the Option.

  4. Where may the question mark on an Option be used?

    Answer

    In a function that returns Option

    The operator returns None from the enclosing function, so its return type must accept that.

  5. What does this program print? fn main() { let values = [10, 20, 30]; match values.get(1) { Some(value) => println!("found {value}"), None => println!("nothing"), } }

    Answer

    found 20

    Index one is present, and the Some arm prints the value it holds.

  6. What does this program print? fn main() { let some: Option<i32> = Some(5); let none: Option<i32> = None; println!("{} {}", some.unwrap_or(0), none.unwrap_or(0)); println!("{:?}", some.map(|value| value * 2)); }

    Answer

    5 0 / Some(10)

    The fallback applies only to the empty one, and the map prints the wrapped double.

  7. What does this program print? fn main() { let name: Option<&str> = Some("Rust"); if let Some(name) = name { println!("hello {name}"); } }

    Answer

    hello Rust

    The value is present, the pattern binds it, and the block prints it.

  8. What does this program print? fn first_char(text: &str) -> Option<char> { let first = text.chars().next()?; Some(first) } fn main() { println!("{:?}", first_char("rust")); println!("{:?}", first_char("")); }

    Answer

    Some('r') / None

    The first string has a character, and the empty one makes the question mark return None.

  9. What is the first line of the compiler output for this program? fn main() { let total = Some(3) + 1; println!("{total}"); }

    Answer

    error[E0369]: cannot add {integer} to Option<{integer}>

    The operator has no implementation for Option, and the message shows both operand types.

  10. What does this program print? fn main() { let value: Option<i32> = None; println!("{}", value.is_some()); println!("{}", value.is_none()); }

    Answer

    false / true

    The value is empty, so is_some is false and is_none is true.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev