Skip to editor content
learningrust.orglesson 10 of 26

Option and Result

Two enums sit at the heart of almost every Rust program you will ever write: Option<T> and Result<T, E>. They replace null pointers and unchecked exceptions with types the compiler enforces. Once you internalise both, you gain a superpower: the compiler tells you exactly where something might be absent or might fail, and refuses to compile until you have handled it.

Option — Values That May Not Exist

Option<T> models the idea of "a value that is either present or not". The standard library defines it as:

enum Option<T> {
    Some(T), // a value is present
    None,    // nothing is here
}

Returning None is Rust's safe alternative to returning null. Because the type system tracks whether a value is wrapped in Option, you can never accidentally use a "null" value without checking it first.

Basic Option Usage

fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn main() {
    let odds  = vec![1, 3, 5, 7];
    let mixed = vec![1, 3, 4, 6];

    match find_first_even(&odds) {
        Some(n) => println!("Found even number: {}", n),
        None    => println!("No even numbers in the list"),
    }

    match find_first_even(&mixed) {
        Some(n) => println!("Found even number: {}", n),
        None    => println!("No even numbers in the list"),
    }
}

match is the most explicit way to handle an Option, but Rust provides several concise combinators for common patterns.

Option Combinators

Combinators let you transform and chain Option values without nested match blocks:

fn username_initials(name: &str) -> Option<String> {
    // Split on whitespace, collect first letters, return None if no words
    let initials: String = name
        .split_whitespace()
        .filter_map(|word| word.chars().next())
        .map(|c| c.to_ascii_uppercase())
        .collect();

    if initials.is_empty() { None } else { Some(initials) }
}

fn main() {
    let names = vec!["alice bob", "", "carol  dan  eve"];

    for name in &names {
        // map: transform the Some value, leaving None untouched
        let display = username_initials(name)
            .map(|i| format!("Initials: {}", i))
            .unwrap_or_else(|| String::from("(no name)"));

        println!("{:?} => {}", name, display);
    }

    // unwrap_or gives a default without panicking
    let score: Option<u32> = None;
    println!("Score: {}", score.unwrap_or(0));

    // and_then chains operations that each return an Option
    let raw: Option<&str> = Some("  42  ");
    let parsed: Option<u32> = raw
        .map(str::trim)
        .and_then(|s| s.parse().ok());
    println!("Parsed: {:?}", parsed);
}

Key combinators to know:

CombinatorWhat it does
map(f)Transform Some(x) to Some(f(x)); pass None through
and_then(f)Chain: call f(x) which itself returns an Option
unwrap_or(default)Extract the value or use the fallback
unwrap_or_else(f)Like unwrap_or but the fallback is lazily computed
filter(pred)Keep Some(x) only if pred(x) is true

Result — Operations That Can Fail

Result<T, E> models a computation that either succeeds with a value of type T or fails with an error of type E:

enum Result<T, E> {
    Ok(T),  // success
    Err(E), // failure, with an error value
}

Any function that can fail should return Result instead of panicking. Callers are then forced by the compiler to decide what to do with errors.

Defining and Returning Results

#[derive(Debug)]
enum ParseError {
    Empty,
    InvalidFormat(String),
    OutOfRange(i32),
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::Empty             => write!(f, "input was empty"),
            ParseError::InvalidFormat(s)  => write!(f, "invalid format: {}", s),
            ParseError::OutOfRange(n)     => write!(f, "value {} is out of range (1–100)", n),
        }
    }
}

fn parse_score(input: &str) -> Result<i32, ParseError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(ParseError::Empty);
    }
    let n: i32 = trimmed
        .parse()
        .map_err(|_| ParseError::InvalidFormat(trimmed.to_string()))?;
    if !(1..=100).contains(&n) {
        return Err(ParseError::OutOfRange(n));
    }
    Ok(n)
}

fn main() {
    let inputs = ["42", "", "abc", "200", " 77 "];
    for input in &inputs {
        match parse_score(input) {
            Ok(score) => println!("{:?} => score {}", input, score),
            Err(e)    => println!("{:?} => error: {}", input, e),
        }
    }
}

Notice the ? at the end of the .parse() call. If the parse fails, ? returns the Err early, after running map_err to convert the error type. This is the idiomatic Rust way to propagate errors.

The ? Operator

The ? operator is shorthand for "if this is an Err (or None), return it immediately; otherwise give me the inner value." It dramatically reduces boilerplate in functions that call multiple fallible operations in sequence:

use std::num::ParseIntError;

fn add_str_numbers(a: &str, b: &str) -> Result<i32, ParseIntError> {
    // ? propagates the error and returns early if parsing fails
    let x: i32 = a.trim().parse()?;
    let y: i32 = b.trim().parse()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", add_str_numbers("10", "32"));   // Ok(42)
    println!("{:?}", add_str_numbers("10", "oops")); // Err(...)
    println!("{:?}", add_str_numbers(" 5 ", " 5 ")); // Ok(10)
}

? works identically for Option inside functions that return Option.

Converting Between Option and Result

Because Option and Result model related concepts, the standard library provides easy conversions:

// Option → Result
let opt: Option<i32> = Some(42);
let res: Result<i32, &str> = opt.ok_or("missing value");

// Result → Option (discards the error)
let res2: Result<i32, &str> = Ok(42);
let opt2: Option<i32> = res2.ok();

ok_or is particularly useful when you want to treat a missing value as an error in a Result-returning function.

Try It Yourself

Extend the function below so that it:

  1. Looks up a user's score from the map (returning None as an error if the user is not found).
  2. Parses the stored string value as a u32.
  3. Returns Err with a descriptive message for any failure.
use std::collections::HashMap;

fn get_score(db: &HashMap<&str, &str>, user: &str) -> Result<u32, String> {
    // Step 1: look up the user — return Err if not found
    let raw = db
        .get(user)
        .ok_or_else(|| format!("user '{}' not found", user))?;

    // Step 2: parse the string as a number — return Err if invalid
    let score: u32 = raw
        .trim()
        .parse()
        .map_err(|_| format!("'{}' is not a valid score for user '{}'", raw, user))?;

    Ok(score)
}

fn main() {
    let mut db = HashMap::new();
    db.insert("alice", "95");
    db.insert("bob",   "bad_data");

    let users = ["alice", "bob", "carol"];
    for user in &users {
        match get_score(&db, user) {
            Ok(s)  => println!("{}: score = {}", user, s),
            Err(e) => println!("{}: error — {}", user, e),
        }
    }
}

Try adding more entries to the map, or deliberately breaking a score value, to see the different error paths activate.

Key Takeaways

  • Option<T> replaces null: use Some(value) or None to represent optional data.
  • Result<T, E> replaces exceptions: use Ok(value) or Err(error) for fallible operations.
  • match handles both types exhaustively; combinators like map, and_then, and unwrap_or keep code concise.
  • The ? operator propagates errors (or None) early, eliminating most boilerplate error-forwarding code.
  • Convert between the two with .ok() (Result → Option) and .ok_or(e) (Option → Result).
  • Prefer descriptive custom error types over plain String errors in library code.
  • Never use .unwrap() in production code paths; always handle or propagate errors explicitly.

Pro Tip: Reach for and_then when you need to chain multiple Option- or Result-returning operations. It reads like a pipeline: each step only runs if the previous one succeeded, and the first failure short-circuits the whole chain — giving you the same guarantees as the ? operator but as a functional expression you can embed in an iterator chain or assignment.

Next Steps

Now that you understand Option and Result, you're ready to learn about error handling in depth — including custom error types, the ? operator in practice, and strategies for robust error management.

Next lesson

Error Handling

Learn Rust error handling with the Result type, panic!, recoverable vs unrecoverable errors, and the ? operator for propagation

20 min