---
title: "11.3 Option and the end of null"
description: "Option<T> is Rust's answer to the billion-dollar null mistake: absence is a value the compiler forces you to handle, and unwrap/expect are finally explained in full."
url: "https://learnrust.net/chapter-11/option/"
last_updated: "2026-06-13"
---

# 11.3 Option and the end of null

Most languages have a special value that means "nothing here": `null`, `nil`, `None`, `nullptr`. It seems harmless, even necessary. It is, in fact, the source of an enormous share of the world's software crashes, and Rust doesn't have it. In its place is a data-carrying enum (lesson [11.2](https://learnrust.net/chapter-11/enums-with-data/)) that turns "there might be nothing here" into a fact the compiler refuses to let you ignore.

## The billion-dollar mistake

Tony Hoare, who introduced null references into a language in 1965, later called it his "billion-dollar mistake":

> I call it my billion-dollar mistake. It was the invention of the null reference in 1965... This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

The problem isn't the *idea* of absence; programs genuinely need to express "no value here" (the user typed nothing, the search found no match, the list is empty). The problem is that in most languages, *any* value of *any* type can secretly be null, and the compiler doesn't make you check. You write `customer.email.length`, the email is null, and the program crashes at runtime, in production, far from where the null came from. The type said `email`, it lied, and nobody found out until a user did.

## Option makes absence a type

Rust's fix is to make absence visible *in the type*. A value that might be missing has type `Option<T>`, an enum with exactly two variants:

```rust
enum Option<T> {
    Some(T),
    None,
}
```

`Some(T)` carries a value of some type `T`; `None` carries nothing and means "absent." The `<T>` is a placeholder for whatever type the value is: `Option<i32>` is "maybe an `i32`," `Option<String>` is "maybe a `String`." (That `<T>` is generics, chapter 15's subject; for now read `Option<i32>` as one word meaning "an i32 that might not be there.") `Option` is so fundamental that it's in scope everywhere automatically; you write `Some(5)` and `None` without any imports.

```rust
fn main() {
    let some_number: Option<i32> = Some(5);
    let nothing: Option<i32> = None;

    println!("{some_number:?}");
    println!("{nothing:?}");
}
```

```
Some(5)
None
```

Here's the crucial part. An `Option<i32>` is *not* an `i32`. They are different types, and the compiler will not let you use one as the other:

```rust
fn main() {
    let x: i32 = 5;
    let y: Option<i32> = Some(5);
    let sum = x + y;
}
```

```
error[E0277]: cannot add `Option<i32>` to `i32`
 --> src/main.rs:4:17
  |
4 |     let sum = x + y;
  |                 ^ no implementation for `i32 + Option<i32>`
```

This error is the whole idea in one transcript. You cannot accidentally use a maybe-absent value as if it were definitely present, because the *types don't match*. To add `y`, you must first deal with the possibility that it's `None`. The compiler has moved the null check from "something you hope you remembered" to "something the program won't compile without."

> **Key insight**
>
> In most languages, every value is secretly nullable and the compiler trusts you to check. In Rust, a value is non-null by default, and the *only* way to express "might be absent" is `Option<T>`, which the compiler then forces you to unwrap before use. The billion-dollar mistake was making absence invisible. `Option` makes it a type you can see and the compiler can count.

## Getting the value out: match

Since `Option<T>` is an enum, you handle it the way you handle any enum: with `match` (lesson [11.2](https://learnrust.net/chapter-11/enums-with-data/)). This forces you to write what happens in *both* cases, present and absent:

```rust
fn describe(maybe_age: Option<u32>) -> String {
    match maybe_age {
        Some(age) => format!("age is {age}"),
        None => String::from("age unknown"),
    }
}

fn main() {
    println!("{}", describe(Some(30)));
    println!("{}", describe(None));
}
```

```
age is 30
age unknown
```

The `Some(age)` arm only runs when there's a value, and it binds that value to `age` so you can use it safely. The `None` arm handles absence. There is no path through this code where you touch a missing value, and the compiler checked that by exhaustiveness. The crash that null causes simply has nowhere to happen.

## unwrap and expect, explained at last

Way back in lesson [1.6](https://learnrust.net/chapter-1/reading-input/) you wrote `.expect("...")` after `read_line` on a trust-us IOU, and you've used it since without a full explanation. Here it is. `read_line` and `parse` (lesson [5.6](https://learnrust.net/chapter-5/parsing-strings-into-numbers/)) return values that *might have failed*, and `Option`'s cousin `Result` (chapter 12) wraps them the same way `Option` wraps maybe-absence. `Option` itself has the two methods you've been calling:

`unwrap()` says "I'm certain this is `Some`; give me the value, and if it's `None`, crash." `expect("message")` does the same but lets you supply the panic message, which is why lesson [3.x](https://learnrust.net/chapter-3/chapter-3-summary-and-quiz/) called `expect`'s text "your words at the crash site."

```rust
fn main() {
    let x: Option<i32> = Some(5);
    println!("{}", x.unwrap());        // 5

    let y: Option<i32> = None;
    println!("{}", y.unwrap());        // panics
}
```

```
5

thread 'main' panicked at src/main.rs:6:22:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

So `unwrap` and `expect` are the escape hatch: they convert "maybe absent" into "definitely present, or a panic." They're convenient and they're a trap, in equal measure. Every `unwrap` is a place your program will crash if you're wrong about `None` never happening.

> **Best practice**
>
> Prefer `match` (or the lighter tools in lesson [11.6](https://learnrust.net/chapter-11/if-let-while-let-let-else/)) over `unwrap`/`expect` for `Option`s that can really be `None`. Reserve `unwrap` for cases where `None` is genuinely impossible, and reach for `expect` over `unwrap` when you do use it, so the crash explains *why* you believed it couldn't happen. Chapter 12 returns to this judgment in full for `Result`.

## A taste of the convenience methods

Writing a full `match` for every `Option` would be tiring, so `Option` carries dozens of methods for common patterns. A few you'll use constantly: `unwrap_or(default)` gives the value or a fallback; `is_some()` and `is_none()` ask which variant it is; `map(...)` transforms the inner value if present and does nothing if absent.

```rust
fn main() {
    let maybe: Option<i32> = None;
    println!("{}", maybe.unwrap_or(0));        // 0, the fallback
    println!("{}", Some(10).unwrap_or(0));     // 10, the value
}
```

```
0
10
```

`unwrap_or` is the safe, no-panic way to say "use this if there's nothing." These methods are why `Option` feels light in practice despite the strictness: the common cases each have a one-word method, and the full `match` is there when you need every case spelled out. Lesson [11.6](https://learnrust.net/chapter-11/if-let-while-let-let-else/) adds `if let` for the "do something only if `Some`" case.

## Quiz time

**Question #1**

Why won't this compile, and what does the refusal prevent?

```rust
fn main() {
    let maybe: Option<i32> = Some(7);
    let doubled = maybe * 2;
    println!("{doubled}");
}
```

<details class="solution">
<summary>Show solution</summary>

`Option<i32>` is not `i32`, so multiplying it by 2 has no implementation (E0277). The refusal forces you to handle the `None` case before using the value, which is exactly the null-check that other languages let you skip. Fix by unwrapping safely, e.g. `maybe.unwrap_or(0) * 2`, or with a `match`.

</details>

**Question #2**

Write a function `first_char(s: &str) -> Option<char>` that returns the first character of a string, or `None` if the string is empty. Then call it on `"hi"` and on `""`, printing both results with `{:?}`.

<details class="solution">
<summary>Show solution</summary>

```rust
fn first_char(s: &str) -> Option<char> {
    s.chars().next()
}

fn main() {
    println!("{:?}", first_char("hi"));   // Some('h')
    println!("{:?}", first_char(""));     // None
}
```

`s.chars().next()` already returns `Option<char>`: the standard library uses `Option` precisely because "the next character" might not exist. Returning `Option<char>` makes the empty case part of the function's type, so every caller is forced to consider it.

</details>

**Question #3**

What does `.unwrap()` do on a `None`, and when is it appropriate to use it?

<details class="solution">
<summary>Show solution</summary>

On `None`, `unwrap()` panics (`called Option::unwrap() on a None value`). It's appropriate only when `None` is genuinely impossible at that point, or in quick throwaway code and tests. For `Option`s that can really be absent, handle both cases with `match`, `if let`, or a method like `unwrap_or`. When you do unwrap, `expect("why this can't be None")` documents your reasoning at the crash site.

</details>

`match` has been doing the heavy lifting for three lessons on a need-to-know basis. The next lesson finally gives `match` its full treatment: binding patterns, ranges, multiple patterns, guards, and the `@` operator.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
