Using map, and_then, or_else, etc.

A guide to transforming Option and Result values in Rust using map, and_then, or_else, and related methods, with practical examples and common pitfalls.

Rust’s Option and Result types are containers for values that might be absent or might represent an error. Working with them often means checking whether the value is Some/Ok or None/Err and acting accordingly. A match expression can do that, but it quickly becomes verbose when you need to chain several operations together.

The standard library provides a set of transformation methods—map, and_then, or_else, map_err, and others—that let you express that logic as a pipeline. Each method focuses on a specific role: transforming a success value, handling an error, or chaining a computation that might itself fail. This page explains how to use them, why they exist, and what goes wrong when you misuse them.

The Mental Model: Containers and Functions

Think of Option<T> and Result<T, E> as boxes. Inside the box is either a value (the T) or some indication of absence/failure. The transformation methods let you work with what is inside the box without opening it manually.

  • map applies a regular function to the inner value. It opens the box, passes the value to your function, and puts the return value back into the same kind of box. If the box is empty (None/Err), nothing happens—the empty box is passed along unchanged.
  • and_then applies a function that itself returns a box. Instead of wrapping the result in another box (which would give you a box inside a box), it flattens the nesting automatically. This is why it is sometimes called flatMap in other languages.
  • or_else is the failure counterpart: it lets you provide a fallback computation that runs only when the box is empty (None/Err). It must return the same kind of box as the original.

All of these methods are lazy about the cases they don't handle: map and and_then ignore the error or absence case entirely, while or_else ignores the success case. That separation allows you to build pipelines where error propagation is automatic and invisible.

Transforming the Success Value with map

map on Option<T>

let maybe_number: Option<i32> = Some(3);
let doubled: Option<i32> = maybe_number.map(|n| n * 2);
assert_eq!(doubled, Some(6));
let none: Option<i32> = None;
let still_none = none.map(|n| n * 2);
assert_eq!(still_none, None);

The closure receives the inner i32 when the option is Some, and returns a new value of any type. map wraps that result in a new Some. If the original is None, the closure is never called and the result remains None.

This is the right tool when you want to convert a value that might be missing into another value that might be missing, and the conversion itself cannot fail independently.

map on Result<T, E>

let ok_value: Result<i32, &str> = Ok(3);
let doubled: Result<i32, &str> = ok_value.map(|n| n * 2);
assert_eq!(doubled, Ok(6));
let err_value: Result<i32, &str> = Err("something went wrong");
let still_err = err_value.map(|n| n * 2);
assert_eq!(still_err, Err("something went wrong"));

Result::map behaves the same way but only for the Ok branch. The error variant is left completely untouched and passed through. This makes it easy to transform a successful outcome while preserving any error information for later.

map only touches the happy path:

A common mistake is expecting map to run on both Ok and Err. It never touches the error. If you need to transform the error as well, you must use map_err separately.

Transforming Only the Error with map_err

Result also offers map_err, which is the mirror image of map. It applies a closure to the error value when the result is Err, and leaves the Ok value alone.

let result: Result<i32, &str> = Err("file not found");
let mapped: Result<i32, String> = result.map_err(|e| format!("Error: {}", e));
assert_eq!(mapped, Err(String::from("Error: file not found")));

This is frequently used to convert one error type into another before propagating with the ? operator, or to enrich an error with additional context.

Order matters when combining map and map_err:

If you call map and then map_err, the error transformation applies to any error that was already present before the map, not to errors that might be created by the map closure (which can't happen because map doesn't create errors). If you need to handle errors from inside a closure that returns a Result, you need and_then.

Chaining Fallible Operations with and_then

When the next step in your pipeline might itself fail—that is, it returns an Option or a Result—using map would give you a nested container: Option<Option<U>> or Result<Result<U, E>, E>. and_then solves that by flattening the structure.

and_then on Option<T>

fn safe_parse(s: &str) -> Option<i32> {
    s.parse().ok()
}
let input: Option<&str> = Some("42");
let parsed = input.and_then(|s| safe_parse(s));
assert_eq!(parsed, Some(42));
let bad_input: Option<&str> = None;
let parsed_none = bad_input.and_then(|s| safe_parse(s));
assert_eq!(parsed_none, None);

If the original Option is None, and_then short-circuits and returns None without calling the closure. If it is Some, the closure is called with the inner value, and its return value—a whole Option—replaces the original. This flattens what would otherwise be Some(Some(...)) or Some(None) into a single Option.

Using map where and_then is needed causes type errors:

If you accidentally write input.map(|s| safe_parse(s)) instead of and_then, the compiler will infer the type Option<Option<i32>>. This will cause downstream mismatches and obscure error messages. The fix is to replace map with and_then.

and_then on Result<T, E>

fn try_double_positive(x: i32) -> Result<i32, &'static str> {
    if x > 0 {
        Ok(x * 2)
    } else {
        Err("value must be positive")
    }
}
let ok: Result<i32, &str> = Ok(5);
let result = ok.and_then(|n| try_double_positive(n));
assert_eq!(result, Ok(10));
let zero: Result<i32, &str> = Ok(0);
let result_err = zero.and_then(|n| try_double_positive(n));
assert_eq!(result_err, Err("value must be positive"));
let already_err: Result<i32, &str> = Err("initial failure");
let still_err = already_err.and_then(|n| try_double_positive(n));
assert_eq!(still_err, Err("initial failure"));

Each and_then receives the success value from the previous step and either continues the pipeline or turns the whole result into an error. This pattern replaces deeply nested match expressions with a linear chain.

Handling the Absence or Error with or_else

Sometimes you want to try an alternative when the first computation fails. or_else does exactly that: it takes a closure that runs only when the container is None or Err, and returns a replacement container of the same type.

or_else on Option<T>

let maybe: Option<&str> = None;
let fallback = maybe.or_else(|| Some("default"));
assert_eq!(fallback, Some("default"));

The closure must return an Option<T> of the same T. This is useful for providing lazy defaults or trying another lookup.

or_else on Result<T, E>

fn try_primary() -> Result<i32, &'static str> {
    Err("primary failed")
}
fn try_secondary() -> Result<i32, &'static str> {
    Ok(42)
}
let outcome = try_primary().or_else(|_err| try_secondary());
assert_eq!(outcome, Ok(42));

On Result, the closure receives the error value, so you can inspect it and decide whether to fall back or to return a different error. This is a powerful pattern for retry logic or for selecting among multiple possible data sources.

Recognising a clean chain:

When you can read your error-handling logic left-to-right as a sequence of map, and_then, and or_else calls, without any explicit match branches, the intent of each step is immediately obvious. If your chain compiles and produces the expected results, you have applied the transforms correctly.

Combining Transforms in Practice

Real-world code rarely uses a single method in isolation. Here is a function that finds a user in a database, extracts their email domain, and falls back to a default if any step fails—all without a single match.

#[derive(Debug)]
struct User {
    email: Option<String>,
}
fn get_domain(db: &[User], index: usize) -> Option<&str> {
    db.get(index)                       // Option<&User>
      .and_then(|user| user.email.as_deref()) // Option<&str>
      .and_then(|email| email.split('@').nth(1)) // Option<&str>
      .or_else(|| Some("unknown.com")) // fallback
}
fn main() {
    let users = vec![
        User { email: Some("alice@example.com".into()) },
        User { email: None },
    ];
    assert_eq!(get_domain(&users, 0), Some("example.com"));
    assert_eq!(get_domain(&users, 1), Some("unknown.com"));
    assert_eq!(get_domain(&users, 2), Some("unknown.com"));
}

Every method targets a specific responsibility: and_then chains operations that might return None, or_else provides a default when the chain has already failed. The absence of match keeps the logical flow clear, even as the number of steps grows.

For Result, the same principles apply, with the addition of map_err to convert errors when necessary:

fn process_config(raw: &str) -> Result<String, String> {
    raw.lines()
       .next()
       .ok_or_else(|| "missing first line".to_string()) // Option -> Result
       .and_then(|line| {
           line.parse::<i32>()
               .map_err(|e| format!("invalid number: {}", e)) // convert error type
               .map(|n| n * 2)
       })
       .map(|doubled| format!("doubled: {}", doubled))
}
assert_eq!(process_config("21\n"), Ok("doubled: 42".to_string()));
assert_eq!(process_config(""), Err("missing first line".to_string()));
assert_eq!(process_config("abc"), Err("invalid number: invalid digit found in string".to_string()));

Do not chain transforms mindlessly:

Long chains can become hard to debug if every step swallows context. Consider adding map_err to enrich errors with information about which step failed, or break the chain into intermediate variables for readability.

Bridging Option and Result

Many pipelines start with an Option and need to end up as a Result (or vice versa). The standard library provides direct conversions:

  • Option::ok_or / ok_or_else — convert None into a specific error value, turning Option<T> into Result<T, E>.
  • Result::ok — discard the error and turn Result<T, E> into Option<T>.
let opt: Option<i32> = Some(5);
let res: Result<i32, &str> = opt.ok_or("no value");
assert_eq!(res, Ok(5));
let opt_none: Option<i32> = None;
let res_err = opt_none.ok_or_else(|| "missing".to_string());
assert_eq!(res_err, Err("missing".to_string()));

These conversions often appear right before a chain of and_then or just after a ? operator. They allow you to choose the error type that makes the most sense for the current context.

Common Pitfalls When Using Transforms

Mistaking map for and_then. This is the single most frequent error. If your closure returns an Option or Result, you almost certainly want and_then, not map. The compiler will complain about type mismatches, but the error message can be confusing because it mentions nested types.

Forgetting that or_else must return the same container type. The closure in or_else has to return Option<T> with the same T, or Result<T, E> with the same T (though E can differ). If you attempt to change the success type, the code won't compile.

Using unwrap as a quick fix. While unwrap and expect are transforms of a sort (they extract the value or panic), they remove the error-handling obligation entirely. In library code, prefer ?, map_err, and and_then so that callers receive a Result they can deal with appropriately.

Over-nesting by chaining map when flattening is needed. If you see Option<Option<...>> or Result<Result<...>>, stop and replace one of the map calls with and_then.

Summary

The transform methods map, and_then, or_else, and map_err turn explicit match-based error handling into a pipeline where each step declares its intent. map changes the success value, map_err changes the error, and_then chains a fallible operation, and or_else provides a fallback when something went wrong. Together they allow you to write error-aware code that stays linear and readable.

The key insight: these methods separate the happy path from the error path at the type level, so your code only deals with one at a time. When you find yourself writing a match that passes through None or Err unchanged, ask whether map or and_then can do the same work in one line.