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.
Option<T>- the archive answering “is it here?” (Some/None)Result<T, E>- the archive answering “what went wrong?” (Ok/Err(reason))- Neither box opens by assumption. The compiler checks for you, at compile time, every time.