Option and Result
The two enums that run the world
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
}
That is all they are. Two tiny enums. And yet almost every function in the Rust standard library returns one of them, and together they eliminate two of the most common bug categories in software.
If something might be absent — "the first element of an empty list", "the value for a missing key" — the function returns Option<T>. If something might fail — "open this file", "parse this number" — it returns Result<T, E>. Rust will not let you touch the inner value until you have said what to do if it is missing or failed.
Option: “there might be a value here”
Option<T> is a box. It contains a T, or it doesn't.
Using an Option:
fn first_word(s: &str) -> Option<&str> {
s.split_whitespace().next()
}
fn main() {
match first_word("hello rust") {
Some(w) => println!("first word is {w}"),
None => println!("no words"),
}
match first_word("") {
Some(w) => println!("first word is {w}"),
None => println!("no words"),
}
}
▶ Run this in the Rust Playground
There is no null in Rust. There never has been. “Maybe absent” is always spelled Option<T>, and the compiler forces you to handle the None case explicitly.
Result: “this might have failed, here’s why”
Result<T, E> is the same idea, but the empty box carries an explanation.
fn parse_age(s: &str) -> Result<u32, std::num::ParseIntError> {
s.trim().parse::<u32>()
}
fn main() {
match parse_age("30") {
Ok(n) => println!("you are {n}"),
Err(e) => println!("that was not a number: {e}"),
}
match parse_age("thirty") {
Ok(n) => println!("you are {n}"),
Err(e) => println!("that was not a number: {e}"),
}
}
▶ Run this in the Rust Playground
Exceptions are mail that bypasses the postal system — they can appear anywhere, surprising you. Result is mail that is addressed to a person. If nobody opens it, your code refuses to compile. You can never forget to handle an error.
The ? operator: the short way to say “propagate the error”
Once you have a chain of fallible calls, writing match for each is noisy. Rust gives you ?:
#![allow(unused)]
fn main() {
use std::fs;
fn read_age(path: &str) -> Result<u32, Box<dyn std::error::Error>> {
let text = fs::read_to_string(path)?; // if this errors, return the error
let n = text.trim().parse::<u32>()?; // if this errors, return the error
Ok(n)
}
}
? means “if this is Ok, give me the inner value and keep going. If this is Err, return it from the enclosing function immediately.” It is the modern, idiomatic way to write fallible Rust. You will see it everywhere.
unwrap and expect: use sparingly, use honestly
You will see code that writes:
#![allow(unused)]
fn main() {
let n = "30".parse::<u32>().unwrap();
}
unwrap says: “I am sure this will be Ok. If I am wrong, crash the program.” expect("a reason") is the same thing but attaches a message. Both are fine in:
- Example code
- Prototypes
- Tests
- Places where you have just checked the value in a way the type system cannot see
They are not fine in production code for things that can fail in the real world (network, files, user input). For those, use ? and handle the error honestly.
If it can fail because of something outside your program, handle it. If it can only fail because you wrote a bug, unwrap is fine.
Here is what an Option actually looks like in memory: a value, not a null pointer waiting to explode.
Interactive simulation (requires JavaScript): Option values are ordinary stack values carrying a present/absent tag plus payload; match must handle both Some and None, checked at compile time.
- Write a function
fn head(v: &[i32]) -> Option<i32>that returns the first element of a slice, orNone. - Write a function
fn safe_div(a: f64, b: f64) -> Result<f64, &'static str>that returnsErr("divide by zero")ifbis zero. - Take the
read_ageexample above and add a call to it inmainthat prints the result cleanly.
You now know enough Rust to build something real. Next: your first CLI.