rust-option-some-none

Option in Rust: handling optional values without null

  • 5 min

Option<T> is a type that represents presence or absence without using null.

Tony Hoare, who introduced null references in ALGOL W, called his creation “the billion dollar mistake”. In many languages, a null reference or pointer can be mistaken for a valid one and cause an exception or invalid access if we forget to check it.

Safe references and ordinary types in Rust do not support null as a valid value. Raw pointers can be null, but they are used outside these guarantees and are common mostly in interoperability.

If a variable is of type String, I guarantee it contains a valid string. If you want to express that there could be nothing, Rust forces you to wrap it in a special type: Option.

What is Option?

Option<T> is an Enum defined in the standard library that has only two variants:

enum Option<T> {
    Some(T), // Contains a value of type T
    None,    // Contains nothing (equivalent to null, but safe)
}
Copied!

It is included in the prelude, so you can use both Some and None directly without importing anything.

The big difference: type safety

Imagine you have an integer i32. In Rust, that number is ALWAYS valid. If you want an optional number, you have an Option<i32>.

The key is this: they are not the same type. You cannot accidentally sum them.

let x: i8 = 5;
let y: Option<i8> = Some(5);

// ❌ ERROR: cannot add i8 + Option<i8>
// let suma = x + y;
Copied!

The compiler tells you: “Hey, y could be None. I don’t know how to add ‘nothing’ to 5. First make sure there’s data inside”.

This prevents directly confusing absence with value. We can still cause a panic if we force a None with unwrap, but the empty case is visible in the type.

Creating Option values

It is very easy to instantiate this enum:

fn main() {
    let some_number = Some(5);
    let some_text = Some("Hello");

    // If it's None, we must specify the type, because Rust cannot infer it
    let absent_number: Option<i32> = None;
}
Copied!

Extracting the value

You have a box (Option) and you want the gift inside. How do you get it out? We have several strategies depending on how much risk or control we want.

The exhaustive way: match

It is the safest and most explicit way. You handle both cases.

fn greet(name: Option<&str>) {
    match name {
        Some(n) => println!("Hello, {}!", n),
        None => println!("Hello, stranger."),
    }
}
Copied!

The risky way: unwrap and expect

Just like with Result, these methods give you the value if it exists, or cause a panic! if it’s None.

let x = Some(10);
let value = x.unwrap(); // 10

let empty: Option<i32> = None;
// let value = empty.expect("There should have been a number!"); // 💥 Panic
Copied!

Only use this if you are 100% sure it is not None.

A default value with unwrap_or

“Give me the value, and if there is nothing, give me this default value.” This is one of the most useful and clean ways.

let x = Some(10);
let y = None;

// If there is data, use it; otherwise, use 0
let a = x.unwrap_or(0); // 10
let b = y.unwrap_or(0); // 0
Copied!

There is also unwrap_or_else(|| expensive_calculation()), which accepts a closure to calculate the default value only if necessary (lazy evaluation).

The flow control shortcut: if let

If you only care about the Some case and want to ignore None:

if let Some(value) = get_configuration() {
    println!("Configuration loaded: {}", value);
}
Copied!

Transforming Option with combinators

Rust’s functional style fits very well with Option. Imagine you have an Option<i32> and, if it has a value, you want to multiply it by 2. If it’s None, it should remain None.

Imperative way (boring):

let x = Some(5);
let result;

match x {
    Some(i) => result = Some(i * 2),
    None => result = None,
}
Copied!

Functional way (elegant): The .map() method applies a function to the content if it’s Some, and does nothing if it’s None.

let x = Some(5);
let result = x.map(|i| i * 2); // Returns Some(10)

let y: Option<i32> = None;
let nothing = y.map(|i| i * 2); // Returns None
Copied!

We can chain operations without fear of exceptions:

fn name_length(name: Option<String>) -> Option<usize> {
    name
        .map(|s| s.trim().to_string()) // Remove spaces
        .filter(|s| !s.is_empty())     // If empty, convert to None
        .map(|s| s.len())              // Calculate length
}
Copied!

The ? operator with Option

Just like with Result, the ? operator works with Option. If the value is Some, it extracts it. If it’s None, it returns None immediately from the current function.

fn first_element_doubled(list: Vec<i32>) -> Option<i32> {
    // .first() returns Option<&i32>
    let first = list.first()?;

    // If the list was empty, the previous line returned None.
    // If we got here, 'first' holds the value (unwrapped).
    Some(first * 2)
}
Copied!