Stop unwrapping. Start chaining.
Here's a pattern you'll write a hundred times in your first month with Rust. You have an Option<T> or a Result<T, E>. You want to look inside, do something with the value, maybe transform it, maybe pass it to another function that also returns an Option or Result. The naive way is to keep unwrapping, checking, re-wrapping. That's verbose and brittle.
// The clunky way.
let maybe_age: Option<i32> = Some(30);
let doubled = match maybe_age {
Some(age) => Some(age * 2),
None => None,
};
That match works, but you're going to write the same shape over and over: "if Some, do thing, rewrap. if None, pass None along." After the tenth time, you'll start wishing for a shortcut. The shortcut is a family of methods called combinators, and they're how seasoned Rust code stays readable when errors and missing values pile up.
A combinator is just a method that takes a closure and gives you back another Option or Result. You glue them together to describe a whole pipeline of "if everything went well, this is the answer." If anything fails, the pipeline short-circuits and the error or None falls out the bottom unchanged.
map: transform the inside, leave the wrapper
map is the simplest. It says "if I have something, run this closure on it and re-wrap the result. If I have nothing, leave it as nothing."
fn main() {
// Some(5) holds a value, so the closure runs and we get Some(6).
let five = Some(5);
let six = five.map(|x| x + 1);
println!("{six:?}"); // Some(6)
// None holds nothing, so the closure is skipped entirely.
// The result is still None, but with the type the closure would have produced.
let nothing: Option<i32> = None;
let still_nothing = nothing.map(|x| x + 1);
println!("{still_nothing:?}"); // None
}
That's the whole idea. Notice the closure |x| x + 1 only takes the inside value, not the wrapper. You don't write match and you don't write if some.is_some(). The combinator handles the bookkeeping.
Result has the same map, with the same meaning: it transforms the Ok side and passes Err through untouched.
let r: Result<i32, &str> = Ok(5);
let doubled = r.map(|x| x * 2); // Ok(10)
let e: Result<i32, &str> = Err("bad input");
let still_err = e.map(|x| x * 2); // Err("bad input"), closure never ran
and_then: chain operations that themselves might fail
map is great when your transform always succeeds. But what if the transform itself can fail? Say you're parsing a string into a number, then dividing, then parsing again. Each step returns its own Result. If you used map, you'd end up with Result<Result<Result<i32, E>, E>, E> and you'd be unwrapping nested wrappers forever.
That's where and_then earns its keep. It takes a closure that returns another Option (or Result) and flattens automatically. People sometimes call it flat_map in other languages, or bind, or the monad's "then." In Rust standard library, it's just and_then.
// A function that might fail.
fn parse_int(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>()
}
// A function that might also fail (division by zero).
fn divide(x: i32, y: i32) -> Result<i32, &'static str> {
if y == 0 { Err("divide by zero") } else { Ok(x / y) }
}
fn main() {
// We want to parse "10", parse "2", divide them. Each step might fail.
// Convert ParseIntError to &str so all error types match.
let parsed = parse_int("10")
.map_err(|_| "parse failed") // unify error types
.and_then(|x| parse_int("2")
.map_err(|_| "parse failed")
// Now we have x and the second parsed number, divide them.
.and_then(|y| divide(x, y)));
println!("{parsed:?}"); // Ok(5)
}
That's still nested. We'll see in a moment that the ? operator can flatten this further. But the point right now is that and_then lets each step return its own Result without you collecting nested wrappers.
Option::and_then works the same way. Use it when the closure returns another Option, like a lookup that might miss.
use std::collections::HashMap;
let mut users: HashMap<&str, &str> = HashMap::new();
users.insert("alice", "alice@example.com");
// Look up the user, then look up the part of the email before the @.
// Both steps are Options. and_then chains them so we don't nest manually.
let domain = users.get("alice")
.and_then(|email| email.split('@').nth(1)); // Some("example.com")
let missing = users.get("bob")
.and_then(|email| email.split('@').nth(1)); // None, short-circuited
If users.get("bob") is None, the second step never runs. The pipeline gives you None without any extra match.
or_else: fall back when something is missing
The mirror of and_then is or_else. Where and_then runs only on success, or_else runs only on failure. Use it for fallbacks: try the cache, fall back to the database. Try the env var, fall back to the config file.
fn from_env() -> Option<String> {
std::env::var("PORT").ok()
}
fn from_default() -> Option<String> {
Some("8080".to_string())
}
fn main() {
// Try the env var first; if it's None, fall back to the default.
let port = from_env().or_else(from_default);
println!("{port:?}");
}
You can also use unwrap_or and unwrap_or_else to bail out with a value. unwrap_or takes the value directly, unwrap_or_else takes a closure (use the closure form when the fallback is expensive to compute and you don't want to compute it unless needed).
let value = some_option.unwrap_or(0); // cheap default
let value = some_option.unwrap_or_else(|| compute()); // lazy default
map_err: change the shape of the error
When you chain Results with different error types, you need to unify them. map_err transforms the Err side without touching Ok. It's the error-side cousin of map.
let n: Result<i32, std::num::ParseIntError> = "10".parse();
// Convert the parse error into our own error type.
let unified: Result<i32, String> = n.map_err(|e| format!("parse failed: {e}"));
This is the everyday glue between libraries. Library A returns Result<T, ErrA>, library B returns Result<T, ErrB>, and your code wants Result<T, MyError>. map_err is the stitching tool.
ok_or: turn an Option into a Result
Sometimes a missing value should be treated as an error with a message. ok_or converts Option<T> into Result<T, E> by attaching an error for the None case.
let env: Option<&str> = std::env::var("HOME").ok().as_deref(); // (illustrative)
// If we got something, it becomes Ok. If not, we attach our error.
let result: Result<&str, &str> = env.ok_or("HOME is not set");
There's also ok_or_else which takes a closure, in case constructing the error value is expensive.
A more realistic pipeline
Let's pull these together. Imagine you're parsing config: read an environment variable, parse it as a number, validate it's positive, return the value or an error.
// Each step is its own small function. Combinators wire them up.
fn parse_port(raw: Option<String>) -> Result<u16, String> {
raw
// None -> Err("missing")
.ok_or_else(|| "PORT is not set".to_string())
// Convert Result<String, String> into Result<u16, String>.
// and_then because parsing itself returns a Result.
.and_then(|s| s.parse::<u16>()
.map_err(|e| format!("PORT is not a valid number: {e}")))
// Validate: closure returns Result, so and_then again.
.and_then(|p| if p > 0 {
Ok(p)
} else {
Err("PORT must be greater than zero".to_string())
})
}
fn main() {
println!("{:?}", parse_port(Some("8080".into()))); // Ok(8080)
println!("{:?}", parse_port(Some("0".into()))); // Err("PORT must be...")
println!("{:?}", parse_port(Some("oops".into()))); // Err("PORT is not a...")
println!("{:?}", parse_port(None)); // Err("PORT is not set")
}
Notice how each step says exactly what it does. There's no if let Some(...) inside if let Ok(...) ladder. Failure modes are named close to where they happen. And if any step fails, the rest are skipped automatically.
When the ? operator is better
The ? operator is sugar for "if this is Err, return early; if it's Ok, give me the inside value." For straight-line code, ? is shorter and easier to read than a chain of and_thens.
fn parse_port_q(raw: Option<String>) -> Result<u16, String> {
let s = raw.ok_or_else(|| "PORT is not set".to_string())?;
let p = s.parse::<u16>().map_err(|e| format!("invalid: {e}"))?;
if p == 0 { return Err("must be positive".into()); }
Ok(p)
}
Combinators are at their best when you want to express a transformation as a single expression, especially when assigning to a variable. ? is at its best for top-level function bodies where each step is a statement. Use whichever makes the surrounding code clearer. They are not opposites. Most real Rust code mixes them.
Common pitfalls
A few traps newcomers fall into.
You used map when you meant and_then. Symptom: the type is Option<Option<T>> or Result<Result<T, E>, E>. The fix: change map to and_then. The closure is returning a wrapper, and map re-wraps it again. and_then flattens.
You forgot to unify error types. Symptom: compiler error E0277 saying "the trait From<X> is not implemented for Y," or "expected MyError, found OtherError." The fix: .map_err(...) to convert, or implement From between your error types so ? can convert automatically.
You used unwrap_or with an expensive default. Symptom: works, but the default is computed every time even when not needed. The fix: use unwrap_or_else(|| compute()) with a closure so the default is only built when actually used.
You overdo it. Symptom: a 12-line combinator chain that nobody can read. The fix: break it into named intermediate variables, or switch to ? for the linear parts.
Where to go next
Combinators are the connective tissue of idiomatic Rust error handling. Once you can read and write a chain of map, and_then, or_else, and map_err without thinking, the rest of the language gets a lot more pleasant.