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)becomesv;Err(e)returnsErr(From::from(e))— the error is converted to the function’s error type withFrom, which is what makes error-type composition work; -
on an
Option<T>:Some(v)becomesv;NonereturnsNone.
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 |
|---|---|---|
|
both |
transform the success value: |
|
|
transform the error, e.g. to add context or convert types |
|
both |
chain an operation that itself returns |
|
both |
fall back to another |
|
|
convert to |
|
|
convert to |
|
both |
extract with a fallback |
|
|
keep the value only if the predicate holds |
|
|
|
|
|
move the value out or swap it in place |
|
both |
|
|
both |
|
|
both |
|
|
both |
swap the nesting: |
|
both |
|
|
both |
look at the value (for logging) and pass it on (1.76) |
|
both |
test the value with a predicate |
|
|
|
|
|
|
Source: std::option::Option and std::result::Result
#[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(())
}
Source: std: Option::transpose
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
expectwith 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.
References
-
std::option and Option
-
std::result and Result
-
std: bool (then, then_some, ok_or)
-
The Rust Programming Language: Recoverable Errors with Result
-
Rust by Example: Option & unwrap (?, combinators: map, and_then, defaults)
-
Rust by Example: Result (map for Result, aliases, early returns, ?)