Option, Result & Combinators

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Option<T> represents a value that may be absent; Result<T, E> represents an operation that may fail with an error of type E. Both are plain enums (see Enums & Pattern Matching), and the standard library gives them a rich API of combinators — methods that transform them without a match — plus the ? operator for early return. Designing error types is the subject of the next page, Error Handling.

The two types

enum OptionLike<T> { None, Some(T) }        // std::option::Option
enum ResultLike<T, E> { Ok(T), Err(E) }     // std::result::Result

Source: std::option

Both, and their variants, are in the prelude. Use Option when absence is normal and needs no explanation (a lookup that finds nothing, an optional field); use Result when the caller needs to know why something failed. Both are marked #[must_use], so ignoring a returned Result is a warning.

use std::collections::HashMap;

fn find_year(catalog: &HashMap<&str, u16>, title: &str) -> Option<u16> {
    catalog.get(title).copied()
}

fn parse_year(text: &str) -> Result<u16, std::num::ParseIntError> {
    text.trim().parse()
}

fn main() {
    let catalog = HashMap::from([("Dune", 1965), ("Emma", 1815)]);
    match find_year(&catalog, "Dune") {
        Some(y) => println!("Dune: {y}"),
        None => println!("not in catalog"),
    }
    match parse_year("19x5") {
        Ok(y) => println!("{y}"),
        Err(e) => println!("bad year: {e}"), // bad year: invalid digit found in string
    }
    println!("{} {}", find_year(&catalog, "Ulysses").is_none(), parse_year("1965").is_ok());
    println!("{}", find_year(&catalog, "Emma").is_some_and(|y| y < 1900)); // true
}

Source: std::result

The ? operator

expr? unwraps a success value or returns early from the enclosing function:

  • on a Result<T, E>: Ok(v) becomes v; Err(e) returns Err(From::from(e)) — the error is converted to the function’s error type with From, which is what makes error-type composition work;

  • on an Option<T>: Some(v) becomes v; None returns None.

The enclosing function must return a compatible type, otherwise you get error E0277 ("the ? operator can only be used in a function that returns Result or Option`"). `? does not convert between Option and Result; use ok_or (Option to Result) or .ok() (Result to Option) first.

use std::collections::HashMap;

#[derive(Debug)]
struct Book {
    title: String,
    author_id: u32,
}

fn author_of(books: &HashMap<String, Book>, authors: &HashMap<u32, String>, isbn: &str) -> Option<String> {
    let book = books.get(isbn)?;               // None -> return None
    let name = authors.get(&book.author_id)?;
    Some(format!("{} by {name}", book.title))
}

fn total_pages(lines: &[&str]) -> Result<u32, std::num::ParseIntError> {
    let mut total = 0;
    for line in lines {
        total += line.trim().parse::<u32>()?;  // Err -> return Err
    }
    Ok(total)
}

fn main() {
    let books = HashMap::from([("978".to_string(), Book { title: "Dune".into(), author_id: 1 })]);
    let authors = HashMap::from([(1, "Frank Herbert".to_string())]);
    println!("{:?}", author_of(&books, &authors, "978"));  // Some("Dune by Frank Herbert")
    println!("{:?}", author_of(&books, &authors, "000"));  // None
    println!("{:?}", total_pages(&["412", "180"]));         // Ok(592)
    println!("{:?}", total_pages(&["412", "many"]));        // Err(ParseIntError { kind: InvalidDigit })
}

Combinators

Combinators express "do this to the value if there is one" without nesting match expressions. They are lazy where it matters: the closure passed to map, and_then or unwrap_or_else runs only if needed. The most important ones, grouped by purpose:

Method On Purpose

map(f)

both

transform the success value: Some(x) → Some(f(x)), Ok(x) → Ok(f(x))

map_err(f)

Result

transform the error, e.g. to add context or convert types

and_then(f)

both

chain an operation that itself returns Option/Result (flat-map)

or / or_else(f)

both

fall back to another Option/Result if empty or failed

ok_or(e) / ok_or_else(f)

Option

convert to Result, supplying the error

ok() / err()

Result

convert to Option, discarding the other side

unwrap_or(v) / unwrap_or_else(f) / unwrap_or_default()

both

extract with a fallback

filter(pred)

Option

keep the value only if the predicate holds

zip(other)

Option

Somea, b if both are Some

take() / replace(v) / insert(v) / get_or_insert_with(f)

Option

move the value out or swap it in place

as_ref() / as_mut()

both

&Option<T> → Option<&T>, so you can inspect without moving

as_deref()

both

Option<String> → Option<&str>, Option<Box<T>> → Option<&T>

copied() / cloned()

both

Option<&T> → Option<T>

transpose()

both

swap the nesting: Option<Result<T, E>> <→ Result<Option<T>, E>

flatten()

both

Option<Option<T>> → Option<T>

inspect(f) / inspect_err(f)

both

look at the value (for logging) and pass it on (1.76)

is_some_and(pred) / is_none_or(pred) / is_ok_and(pred)

both

test the value with a predicate

bool::then(f) / then_some(v)

bool

true → Some(...), false → None

bool::ok_or(e) / ok_or_else(f) (1.98)

bool

true → Ok(()), false → Err(e)

#[derive(Debug)]
struct Member {
    name: String,
    nickname: Option<String>,
    max_loans: Option<u8>,
}

fn display_name(m: &Member) -> &str {
    m.nickname.as_deref().filter(|n| !n.is_empty()).unwrap_or(&m.name)
}

fn parse_rating(input: &str) -> Result<u8, String> {
    input
        .trim()
        .parse::<u8>()
        .map_err(|e| format!("{input:?} is not a number: {e}"))
        .and_then(|n| if (1..=5).contains(&n) { Ok(n) } else { Err(format!("{n} is out of range")) })
}

fn main() {
    let alice = Member { name: "Alice".into(), nickname: Some(String::new()), max_loans: None };
    let bob = Member { name: "Robert".into(), nickname: Some("Bob".into()), max_loans: Some(8) };
    println!("{} {}", display_name(&alice), display_name(&bob));       // Alice Bob

    let limit = alice.max_loans.unwrap_or(5);                            // fallback value
    let label = bob.max_loans.map_or("default".to_string(), |n| format!("{n} loans"));
    println!("{limit} {label}");

    println!("{:?} {:?} {:?}", parse_rating("4"), parse_rating("7"), parse_rating("x"));

    let first = Some("Dune");
    let year = Some(1965);
    println!("{:?}", first.zip(year));                                   // Some(("Dune", 1965))

    let mut reserved: Option<String> = Some("Emma".into());
    let taken = reserved.take();                                         // leaves None behind
    println!("{taken:?} {reserved:?}");

    let in_stock = 3 > 0;
    let checked: Result<(), &str> = in_stock.ok_or("out of stock");     // bool::ok_or, 1.98
    println!("{checked:?} {:?}", (2 > 3).then_some("never"));
}

Chaining instead of nesting

Azam (Advanced Rust, chapter 5) emphasises replacing nested match blocks with combinator chains. Both forms compile to essentially the same code; the chain is shorter but can be harder to read when it grows. A reasonable rule: use combinators for two or three short steps, and ? with named intermediate variables (or let-else) when the logic has branches or needs comments.

fn price_cents(input: &str) -> Option<u32> {
    // nested match
    match input.strip_prefix('$') {
        Some(rest) => match rest.parse::<f64>() {
            Ok(dollars) if dollars >= 0.0 => Some((dollars * 100.0).round() as u32),
            _ => None,
        },
        None => None,
    }
}

fn price_cents_chain(input: &str) -> Option<u32> {
    input
        .strip_prefix('$')
        .and_then(|rest| rest.parse::<f64>().ok())
        .filter(|d| *d >= 0.0)
        .map(|d| (d * 100.0).round() as u32)
}

fn main() {
    for s in ["$12.50", "12.50", "$-3", "$abc"] {
        assert_eq!(price_cents(s), price_cents_chain(s));
        println!("{s:>7} -> {:?}", price_cents_chain(s));
    }
}

Result<Option<T>> versus Option<Result<T>> and transpose

Two nestings carry different meanings:

  • Result<Option<T>, E> — "the operation ran; it may have found nothing" (a database lookup that succeeded but matched no row);

  • Option<Result<T, E>> — "there may or may not have been something to process; if there was, processing may have failed" (an optional field that must parse if present).

transpose converts one into the other, which is what lets ? work on an optional value that needs parsing:

use std::num::ParseIntError;

/// An optional `year` field that must be a valid number if it is present.
fn parse_optional_year(field: Option<&str>) -> Result<Option<u16>, ParseIntError> {
    field.map(|s| s.parse::<u16>()).transpose()
}

fn main() -> Result<(), ParseIntError> {
    println!("{:?}", parse_optional_year(Some("1965"))?); // Some(1965)
    println!("{:?}", parse_optional_year(None)?);         // None
    println!("{:?}", parse_optional_year(Some("x")));     // Err(ParseIntError { kind: InvalidDigit })
    Ok(())
}

unwrap and expect: when they are acceptable

unwrap() returns the value or panics on None/Err; expect("msg") does the same with a message. A panic in a web handler or a library function is a bug (it crashes the thread or the task), so in production code reserve them for:

  • tests and examples, where a panic is a test failure;

  • start-up code, where failing fast is correct (let port: u16 = env::var("PORT")?.parse().expect("PORT must be a number"));

  • proven invariants — the value cannot be absent by construction; then expect with a message that states why ("regex is valid", "checked non-empty above") documents the reasoning.

Everywhere else, propagate with ?, provide a default, or handle the case. Clippy’s unwrap_used and expect_used restriction lints can forbid them in selected crates. The axum section shows how to replace unwrap() in request handlers with an error type (see Axum: Error Handling).

Collecting iterators of Result and Option

collect() can build a Result<Vec<T>, E> from an iterator of Result<T, E>: it stops at the first error and returns it, or returns Ok with all values. The same works for Option, and for sum and product:

fn main() {
    let good = ["1965", "1815", "1987"];
    let bad = ["1965", "oops", "1987"];

    let years: Result<Vec<u16>, _> = good.iter().map(|s| s.parse::<u16>()).collect();
    println!("{years:?}");                    // Ok([1965, 1815, 1987])
    let years: Result<Vec<u16>, _> = bad.iter().map(|s| s.parse::<u16>()).collect();
    println!("{years:?}");                    // Err(ParseIntError { kind: InvalidDigit })

    // Keep the successes and collect the errors separately:
    let (ok, errors): (Vec<_>, Vec<_>) = bad.iter().map(|s| s.parse::<u16>()).partition(Result::is_ok);
    let ok: Vec<u16> = ok.into_iter().map(Result::unwrap).collect();
    println!("{ok:?}, {} errors", errors.len());

    // Just skip the failures:
    let parsed: Vec<u16> = bad.iter().filter_map(|s| s.parse().ok()).collect();
    println!("{parsed:?}");

    let total: Option<u32> = [Some(3), Some(4)].into_iter().sum();
    println!("{total:?}");                    // Some(7)
}

Where the books are outdated

Azam (Advanced Rust, chapter 5) covers map, and_then, or_else, unwrap_or_else, filter and chaining. The standard library has since added is_some_and (1.70), is_none_or (1.82), inspect and inspect_err (1.76), Option::get_or_insert_default, and bool::ok_or/ok_or_else (1.98), which make several of the book’s longer chains shorter.