← Scrolls rust

Option and Result: Schrödinger's Box

Why Rust will not let you carry a lookup away without opening it first

You have looked things up before - in a dictionary, in a phone book, in a database. Sometimes the thing is there. Sometimes it is not. In most languages you shrug and move on: you get the value back, or you get null, or undefined, or nil, and the language trusts you to remember which one you got before you touch it. Rust does not trust you to remember. It makes you look.

The Archive

Picture the city archive - the one with the indifferent stone facade and an archivist who has processed every kind of request and been unimpressed by all of them. You submit a slip for a book. The archivist does not hand you the book. The archivist hands you a sealed box.

Inside is one of two things, and you do not get to know which until you open it: the book itself, or a note explaining why there is no book. If the book was simply never acquired, the note says so, flatly - not held. If something worse happened - the archive burned in ‘89, the ledger was eaten by moths - the note says that instead, and it tells you what went wrong.

The archive has one policy, and it does not bend: you do not read the contents by assuming. You open the box. Every time. It does not care that you are in a hurry.

Rust calls the first kind of box Option. It calls the second kind Result. Both are sealed until opened; they differ only in what the note is allowed to say. Option’s note is always one phrase: not held (None). Result’s note has to explain itself (Err, carrying a reason).

The Habit From Everywhere Else

In most other languages you would just reach into the box - found.title - and trust that something reasonable is on the other side. If the lookup failed, you get null back, and either the language complains the moment you touch it, or it hands you a garbage value and lets you find out three files downstream. That habit is not stupid. It is also the reason the man who invented null calls it, in his own words, his billion-dollar mistake.

struct Book {
    id: u32,
    title: String,
}

fn request_book(id: u32, archive: &[Book]) -> Option<&Book> {
    archive.iter().find(|book| book.id == id)
}

fn main() {
    let archive = vec![Book { id: 1, title: String::from("Reaper Man") }];

    let found = request_book(2, &archive);
    println!("Book: {}", found.title); // reaching into the box without opening it
}
error[E0609]: no field `title` on type `Option<&Book>`
  --> src/main.rs:12:32
   |
12 |     println!("Book: {}", found.title);
   |                                ^^^^^ unknown field
   |
   = note: see the API docs for `Option<T>` for methods that unwrap or inspect its contents

Opening the Box

The compiler is not being difficult. It is the archivist, refusing to let you leave with a sealed box under your arm. Option<&Book> is not a Book that might happen to be missing - it is the box itself, and Rust will not let you get to the contents without opening it.

Open it the way the archive expects: read the note before you act on what it says.

match request_book(2, &archive) {
    Some(book) => println!("Book: {}", book.title),
    None => println!("No book with that id - not held"),
}

Result opens the same way, but its note carries a reason instead of a shrug:

fn parse_priority(input: &str) -> Result<u8, String> {
    input.parse().map_err(|e| format!("not a priority: {e}"))
}

match parse_priority("urgent") {
    Ok(priority) => println!("Priority: {priority}"),
    Err(reason) => println!("Bad priority - {reason}"), // the note explains itself
}

What Changes

You stop reaching into boxes on faith. Every match forces you to answer both branches - the book is there, or it isn’t - and the compiler will not let you leave one unwritten. There is no path left where a missing book quietly becomes a null pointer three files downstream.