Option and Result Combinators
Error Handling in Practice covered ? for propagating errors and match for handling them. Between those two extremes sits a large, underused middle: the combinator methods on Option and Result that transform, chain, and unwrap them without a single match block. Reaching for map, and_then, ok_or, and friends turns nested pyramids of match into flat, readable pipelines, the same shift from imperative to declarative that Iterators Over Loops made for collections. This tutorial covers the combinators that earn their keep and the eager-vs-lazy trap hiding among them.
map and and_then: The Core Distinction
The two most important combinators look similar and are constantly confused. map transforms the inner value with a function that returns a plain value. and_then (aka flatmap) chains a function that itself returns an Option/Result, and flattens the result so you don't end up with Option<Option<T>>.
let text = "42";
// map: the closure returns a plain value → Option<i32> stays one level deep
let len: Option<usize> = Some(text).map(|s| s.len());
// and_then: the closure returns an Option → without flattening you'd get Option<Option<i32>>
let parsed: Option<i32> = Some(text).and_then(|s| s.parse().ok());
The rule: use map when your function can't fail, and_then when it returns another Option/Result. Getting this wrong is the classic Option<Option<T>> (or Result<Result<T, E>, E>) nesting bug, if a .map() gives you a doubly-wrapped type, you wanted .and_then(). This mirrors Iterator::map vs flat_map exactly, and for the same reason.
Replacing match Pyramids with Chains
The payoff is dissolving nested match. Consider looking up a user, then their config, then a field, each step fallible:
// naive: a pyramid of match, three levels deep
let timeout = match find_user(id) {
Some(user) => match user.config() {
Some(cfg) => match cfg.timeout {
Some(t) => t,
None => 30,
},
None => 30,
},
None => 30,
};
// idiomatic: a flat chain that reads top to bottom
let timeout = find_user(id)
.and_then(|user| user.config())
.and_then(|cfg| cfg.timeout)
.unwrap_or(30);
Each and_then short-circuits: the moment any step yields None, the rest are skipped and the chain produces None, exactly like ? but as an expression you can keep transforming. The final unwrap_or(30) supplies the fallback. The chain is not just shorter; it removes the repeated None => 30 arms where a copy-paste mistake could hide.
Bridging Option and Result
Option and Result model different things (absence vs failure), but real code constantly converts between them, usually at the point where "missing" becomes an error worth reporting. Two methods do this:
// ok_or: Option → Result, supplying the error for the None case
let name: Result<String, MyError> = maybe_name.ok_or(MyError::MissingName);
// .ok(): Result → Option, discarding the error (when you don't care why it failed)
let parsed: Option<i32> = "42".parse::<i32>().ok();
ok_or (and its lazy sibling ok_or_else) is the idiomatic way to turn a None into a real error so it can flow through ? and your error libraries. Going the other way, .ok() drops a Result's error to get an Option, useful in an and_then chain where the reason for failure doesn't matter. map_err transforms the error type without touching the success value, the bridge that makes a foreign error fit your own enum when #[from] isn't set up.
Eager vs Lazy: The _or / _or_else Trap
Many combinators come in two forms: one taking a value (unwrap_or, ok_or, map_or) and one taking a closure (unwrap_or_else, ok_or_else, map_or_else). They produce the same result, but differ in when the fallback is computed.
// unwrap_or: the argument is evaluated ALWAYS, even when self is Some
let config = maybe_config.unwrap_or(load_default_config()); // load_default_config() ALWAYS runs
// unwrap_or_else: the closure runs ONLY when self is None
let config = maybe_config.unwrap_or_else(|| load_default_config()); // runs only if needed
Gotcha:
unwrap_or(expensive())evaluatesexpensive()unconditionally, beforeunwrap_oris even called, because arguments are evaluated eagerly in Rust. If the fallback is a function call, an allocation, or anything non-trivial, you're paying for it on every success path where it's thrown away. Use the_or_else(closure) variant for any computed fallback so the work happens only when actually needed. Reserve the plain_orform for cheap constants (unwrap_or(0),unwrap_or("")). This is the same eager-argument trap as.with_context()vs.context()in the anyhow tutorial.
Handy Specialized Combinators
A few more that replace specific match shapes:
// filter: keep Some only if a predicate holds, else None
let even = Some(4).filter(|n| n % 2 == 0); // Some(4); Some(3) → None
// or / or_else: supply an alternative Option/Result if self is None/Err
let value = primary.or(fallback); // first Some wins
// transpose: swap the nesting of Option<Result<..>> ↔ Result<Option<..>>
let x: Result<Option<i32>, _> = Some("5".parse()).transpose();
// unwrap_or_default: use the type's Default when None/Err
let count: u32 = maybe_count.unwrap_or_default(); // 0
transpose is the specialist that resolves a genuinely awkward case: an Option<Result<T, E>> (e.g. "maybe I have a value, and parsing it might fail") flips into Result<Option<T>, E> so it slots into a ? chain. filter turns a value-plus-predicate into an Option without an if. These are worth recognizing rather than memorizing, when a match feels boilerplate-y, there's usually a named combinator for it.
Combinator Cheat Sheet
| You want | Use |
|---|---|
| Transform the inner value (can't fail) | map |
Chain a step that returns Option/Result | and_then |
| Provide a fallback value | unwrap_or (cheap) / unwrap_or_else (computed) |
Turn None into an error | ok_or / ok_or_else |
Turn Result into Option (drop error) | .ok() |
| Change the error type | map_err |
Keep Some only if a predicate holds | filter |
Fall back to another Option/Result | or / or_else |
Flip Option<Result> ↔ Result<Option> | transpose |
Use Default when empty | unwrap_or_default |
The guiding principle: combinators are to match what iterator adapters are to for loops. A single match on an Option/Result is fine; but nested or repetitive matching is a signal to reach for a chain. And whenever a combinator has an _else twin, prefer it for any non-trivial fallback so the work stays lazy.
Key Takeaways
maptransforms the inner value with a function returning a plain value;and_thenchains a function returning anotherOption/Resultand flattens it. A doubly-wrapped type (Option<Option<T>>) means you wantedand_then.- Chaining
and_thenshort-circuits on the firstNone/Err, collapsing nestedmatchpyramids into a flat pipeline that reads top-to-bottom, finished withunwrap_or/ok_orfor the fallback. - Bridge the two types at the "absence becomes an error" boundary:
ok_or/ok_or_else(Option→Result),.ok()(Result→Option), andmap_errto fit a foreign error into your own enum. - The
_orvs_or_elsesplit is eager vs lazy:unwrap_or(expensive())runsexpensive()unconditionally. Use the closure (_or_else) form for any computed fallback; reserve_orfor cheap constants. - Recognize the specialists,
filter,or/or_else,transpose,unwrap_or_default, as replacements for specificmatchshapes. Combinators are tomatchwhat iterator adapters are to loops.