Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Option and Result

If you only remember one thing: in Rust, "might be missing" and "might have failed" are normal values the compiler will not let you ignore.

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.

Plain English

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”

Two boxes

Option<T> is a box. It contains a T, or it doesn't.

Some(T) T None empty

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

Analogy

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.

Plain English

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.

## Try this
Five-minute exercises
  1. Write a function fn head(v: &[i32]) -> Option<i32> that returns the first element of a slice, or None.
  2. Write a function fn safe_div(a: f64, b: f64) -> Result<f64, &'static str> that returns Err("divide by zero") if b is zero.
  3. Take the read_age example above and add a call to it in main that prints the result cleanly.

You now know enough Rust to build something real. Next: your first CLI.

Your first CLI →