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

Intermediate · Abstractions

What & why

Most languages let a function secretly fail — throw an exception, return null, crash — and you find out at 2am. Rust makes failure part of the return type, so the compiler forces you to deal with it before your program runs. Option<T> says “there might not be a value”; Result<T, E> says “this might fail, and here’s why.” Once you’re comfortable with match on both, the real payoff is their combinators.map(), .and_then(), .unwrap_or(), .ok_or() — which let you chain transformations without unwrapping early and re-wrapping by hand.

The idea, slowly

Option: maybe there’s a value, maybe not

Option<T> is Rust’s honest answer to “this might have nothing.” It has exactly two shapes: Some(value) (there’s a value) or None (there isn’t). This is what replaces null — but unlike null, you can’t accidentally use it as if a value were there, because the compiler makes you check.

fn main() {
    let names = vec!["Alice", "Bob"];

    match names.get(5) {         // .get returns Option: Some or None
        Some(name) => println!("found {}", name),
        None => println!("nobody at index 5"),
    }
}

match forces you to write both branches — the found case and the empty case. Forget one and the compiler refuses to build, saying the match isn’t exhaustive. That’s Rust removing an entire category of “I forgot to check for null” bugs.

Result: it worked, or here’s why it failed

Result<T, E> is for operations that can fail with a reason. Its two shapes are Ok(value) (success, here’s the result) and Err(problem) (failure, here’s what went wrong). Parsing text into a number is a classic example — it fails if the text isn’t a number:

fn main() {
    let good: Result<i32, _> = "42".parse();
    let bad: Result<i32, _> = "oops".parse();

    match good {
        Ok(n) => println!("parsed {}", n),
        Err(e) => println!("failed: {}", e),
    }

    match bad {
        Ok(n) => println!("parsed {}", n),
        Err(e) => println!("failed: {}", e), // this one runs
    }
}

Again match makes you handle both outcomes. The Err carries a real error value describing what happened, not just a silent false.

unwrap and expect: the “I’m sure” shortcuts (careful!)

Sometimes you just want the value and are willing to crash if it’s missing. unwrap() and expect(...) do that — they hand back the inner value on success, and panic (crash the program) on None/Err:

fn main() {
    let n: i32 = "42".parse().unwrap(); // fine: it IS a number
    println!("{}", n);

    let ok = "7".parse::<i32>().expect("should be a number");
    println!("{}", ok);
    // "oops".parse::<i32>().unwrap(); // would CRASH the program
}

These are handy in tiny examples and tests. In real programs, reaching for unwrap everywhere means “crash on any problem,” which is rarely what you want. expect is slightly better than unwrap because its message tells you which unwrap blew up — but neither is a substitute for actually handling the failure.

Combinators: transforming without unwrapping

Writing match every time you touch an Option/Result gets verbose, especially when all you want to do is “if there’s a value, transform it” or “if it failed, use a default.” Both types have methods for exactly this — you stay inside the Option/Result “container” the whole time instead of unwrapping, checking, and re-wrapping by hand.

.map() transforms the value inside Some/Ok, leaving None/Err untouched:

fn main() {
    let price: Option<i32> = Some(10);
    let with_tax = price.map(|p| p * 110 / 100);
    println!("{:?}", with_tax); // Some(11)

    let missing: Option<i32> = None;
    let still_missing = missing.map(|p| p * 110 / 100);
    println!("{:?}", still_missing); // None — map never runs the closure
}

.and_then() is for when the next step is itself fallible — the closure you pass must return an Option/Result, not a bare value. This chains fallible steps without nesting Option<Option<T>>:

fn half_if_even(n: i32) -> Option<i32> {
    if n % 2 == 0 { Some(n / 2) } else { None }
}

fn main() {
    let x = Some(8).and_then(half_if_even).and_then(half_if_even);
    println!("{:?}", x); // Some(2)  (8 -> 4 -> 2)

    let y = Some(7).and_then(half_if_even);
    println!("{:?}", y); // None — 7 is odd, chain stops
}

What the compiler is thinking: with .map(f), it expects f: T -> U and wraps the result back in Some/Ok for you. With .and_then(f), it expects f: T -> Option<U> (or Result<U, E>) and does not re-wrap — if your closure returns a bare value instead of Some(value), that’s a type mismatch, not a missing wrap.

.unwrap_or(default) and .unwrap_or_else(|| ...) get you a plain value out, no panic risk — you supply a fallback instead:

fn main() {
    let a: Option<i32> = None;
    println!("{}", a.unwrap_or(0)); // 0

    let b: Result<i32, String> = Err("bad input".to_string());
    println!("{}", b.unwrap_or_else(|_e| -1)); // -1, computed lazily from the error
}

Use .unwrap_or(x) when the fallback is cheap to compute up front; use .unwrap_or_else(|| ...) when computing it is expensive or needs the error value — the closure only runs on the failure path.

.ok_or(err) turns an Option into a Result by supplying the error to use for None. .ok() goes the other way, turning a Result into an Option and throwing away the error:

fn main() {
    let found: Option<i32> = None;
    let as_result: Result<i32, &str> = found.ok_or("not found");
    println!("{:?}", as_result); // Err("not found")

    let parsed: Result<i32, _> = "42".parse();
    let as_option: Option<i32> = parsed.ok();
    println!("{:?}", as_option); // Some(42)
}

.filter() on Option keeps Some(value) only if a predicate returns true; otherwise it becomes None:

fn parse_positive(text: &str) -> Option<i32> {
    text.parse::<i32>().ok().filter(|&n| n > 0)
}

fn main() {
    println!("{:?}", parse_positive("21")); // Some(21)
    println!("{:?}", parse_positive("-5")); // None — filtered out
    println!("{:?}", parse_positive("oops")); // None — parse failed first
}

Chained together, combinators read like a pipeline: text.parse::<i32>().ok().filter(|&n| n > 0).map(|n| n * 2).unwrap_or(0) — parse it, drop it if parsing failed or it’s not positive, double it, or fall back to 0. No match, no intermediate variables.

Common mistakes

  • unwrap() in real code. It crashes the whole program on the first None/Err. Fine for a quick test; risky in anything a user runs. Prefer match, if let, a combinator, or ? (next lesson).
  • Passing .and_then() a closure that returns a bare value instead of Option/Result. .and_then(|n| n + 1) doesn’t compile — the closure must return Some(n + 1) (or Ok/Err). If your closure just transforms the value, you wanted .map(), not .and_then().
  • Reaching for .and_then() when .map() would do. If your closure can’t fail, .map() is simpler and doesn’t need you to wrap the result.
  • Confusing Option with Result. Use Option when something is simply absent (no error to report); use Result when there’s a reason it failed you want to carry. .ok_or() and .ok() exist precisely because this choice sometimes needs to change mid-pipeline.
  • Ignoring a Result entirely. Rust warns if you drop a Result on the floor (unused Result that must be used). Handle it, propagate it, or explicitly let _ = ... if you truly mean to ignore it.

More examples

Chain a parse, then a division that can also fail

Two fallible steps in a row — parsing text, then dividing (which fails on zero) — chain naturally with .and_then() instead of nested match.

fn safe_divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 { None } else { Some(a / b) }
}

fn main() {
    let result = "20".parse::<i32>().ok().and_then(|n| safe_divide(n, 4));
    println!("{:?}", result); // Some(5)

    let by_zero = "20".parse::<i32>().ok().and_then(|n| safe_divide(n, 0));
    println!("{:?}", by_zero); // None
}

Fall back to a sensible default for a missing setting

When a config value is simply absent, .unwrap_or_default() grabs the type’s default (0 for numbers, "" for strings) instead of making you spell out a fallback.

fn main() {
    let raw_config: Option<u32> = None; // key was missing from the config file
    let timeout_secs: u32 = raw_config.unwrap_or_default();

    println!("timeout: {timeout_secs}s"); // 0 -- u32's default
}

Handle one special case by hand, ? for the rest

Not every fallible step deserves the same treatment — here an empty cart is handled explicitly, while everything else flows through ? normally.

fn checkout_total(cart_total: &str) -> Result<i32, String> {
    let total: i32 = match cart_total {
        "0" => return Err("cart is empty".to_string()), // one special case, by hand
        text => text.parse().map_err(|_| "bad total".to_string())?, // normal path uses ?
    };
    Ok(total + 5) // add flat shipping
}

fn main() {
    println!("{:?}", checkout_total("40"));   // Ok(45)
    println!("{:?}", checkout_total("0"));    // Err("cart is empty")
    println!("{:?}", checkout_total("oops")); // Err("bad total")
}

Reject a value that fails a format check

.filter() on Option isn’t just for numeric ranges — it works for any predicate, like rejecting a username that contains spaces.

fn main() {
    let username: Option<&str> = Some("ferris the crab");

    let valid = username.filter(|name| !name.contains(' '));

    println!("{:?}", valid); // None -- contains a space
}

Build a receipt line with chained .map()s

When each step can’t fail, chaining multiple .map() calls reads like a small pipeline — compute a price, then format it, all without unwrapping in between.

fn main() {
    let quantity: Option<i32> = Some(3);

    let receipt_line = quantity
        .map(|q| q * 25)              // price per item is $25
        .map(|total| format!("${total}"));

    println!("{:?}", receipt_line); // Some("$75")
}

Your turn

This function should parse a price string, apply a 10% discount, and fall back to 0 if parsing fails — but it doesn’t compile.

fn discounted_price(text: &str) -> i32 {
    text.parse::<i32>()
        .and_then(|n| n * 90 / 100)
        .unwrap_or(0)
}

fn main() {
    println!("{}", discounted_price("100")); // want: 90
    println!("{}", discounted_price("oops")); // want: 0
}
Show solution

The closure passed to .and_then() must return a Result (since .parse() returns Result<i32, ParseIntError>), but n * 90 / 100 is a bare i32. Since the transformation here can’t fail, the right combinator is .map(), which wraps the output for you:

fn discounted_price(text: &str) -> i32 {
    text.parse::<i32>()
        .map(|n| n * 90 / 100)
        .unwrap_or(0)
}

fn main() {
    println!("{}", discounted_price("100")); // 90
    println!("{}", discounted_price("oops")); // 0
}

.and_then() is for chaining another fallible step (its closure must itself return Result/Option). .map() is for a plain transformation of the success value. Mixing them up is a type error, not a logic bug — the compiler catches it immediately.

Quick check

Remember this

  • Option<T> = Some(v) or None — a value might be missing (Rust’s safe replacement for null).
  • Result<T, E> = Ok(v) or Err(e) — an operation might fail with a reason.
  • match forces you to handle every case, so you can’t forget the failure path.
  • .map(f) transforms the success value (f returns a plain value); .and_then(f) chains another fallible step (f returns Option/Result).
  • .unwrap_or(default) / .unwrap_or_else(|| ...) get a plain value out with a fallback; .ok_or(err) and .ok() convert between Option and Result; .filter() turns Some into None when a predicate fails.
  • unwrap() / expect() grab the value but panic on failure; use them sparingly, mostly in tests and quick scripts.

Go deeper

Next: