The Rust Expression Guide

Chaining Computation with and_then

Last Updated: 2026-04-04

What this page is about

A very common pattern in Rust looks like this: you have an Option<T> or Result<T, E>, and the next step in the computation also returns an Option or Result.

That is the point where map stops being enough.

If the transformation itself is optional or fallible, map preserves the outer wrapper and gives you a nested shape such as Option<Option<T>> or Result<Result<T, E>, E>. Sometimes that nesting is useful, but often it is a smell: the code is expressing one linear computation as stacked containers.

This page introduces and_then, the standard tool for sequencing these operations while keeping the structure flat.

The central idea is simple:

  • use map when the closure returns a plain value
  • use and_then when the closure returns the same kind of wrapper

Once that distinction is clear, a large amount of awkward Rust becomes easier to read.

The core mental model

and_then means: "If there is a usable value, pass it to the next step, and do not wrap the result again."

For Option<T>, the closure returns Option<U>, and the final result is still just Option<U>.

For Result<T, E>, the closure returns Result<U, E> or a compatible Result<U, _> after type shaping, and the final result is still a single Result<U, E>.

That is why and_then is often described as a flattening sequencing operator.

A compact contrast looks like this:

fn with_map(input: Option<&str>) -> Option<Option<u32>> {
    input.map(|s| s.parse::<u32>().ok())
}
 
fn with_and_then(input: Option<&str>) -> Option<u32> {
    input.and_then(|s| s.parse::<u32>().ok())
}

Both closures return Option<u32>. The difference is structural:

  • map preserves the outer Option, producing Option<Option<u32>>
  • and_then sequences the step and keeps the result flat, producing Option<u32>

Why `map` is not enough here

map is perfect when you want to transform the inner value into an ordinary value.

fn trim_name(input: Option<&str>) -> Option<&str> {
    input.map(str::trim)
}

Here the closure returns &str, so map is the right fit.

But if the closure returns another Option, map nests the structure:

fn parse_number(input: Option<&str>) -> Option<Option<u32>> {
    input.map(|s| s.trim().parse::<u32>().ok())
}

That type often signals that the code is conceptually doing one chained computation but is being expressed as a transformation of an already wrapped value into another wrapped value.

Usually the intended meaning is simpler: if there is input, try parsing it; otherwise there is no result.

That is what and_then says directly:

fn parse_number(input: Option<&str>) -> Option<u32> {
    input.and_then(|s| s.trim().parse::<u32>().ok())
}

Using `Option::and_then`

Option::and_then is for computations where each step may produce a value or may fail by producing None.

A simple example:

fn first_nonempty_word(text: Option<&str>) -> Option<&str> {
    text.and_then(|s| s.split_whitespace().next())
}

The closure returns Option<&str> because next() might not find a word. and_then lets that optional step follow the original optional input naturally.

Another example:

fn parse_positive_port(input: Option<&str>) -> Option<u16> {
    input
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<u16>().ok())
        .filter(|&port| port != 0)
}

This reads as a sequence:

  • if there is text, trim it
  • discard empty input
  • try parsing to u16
  • discard zero

Without and_then, the parse step would leave the chain nested and awkward.

Using `Result::and_then`

Result::and_then is the fallible counterpart. It sequences operations where each step may return an error.

fn parse_and_double(input: &str) -> Result<u32, std::num::ParseIntError> {
    input.parse::<u32>().and_then(|n| Ok(n * 2))
}

That example is structurally valid, though the closure is simple enough that map would actually be better there.

A more realistic case is where the second step is also fallible:

fn parse_even(input: &str) -> Result<u32, String> {
    input
        .parse::<u32>()
        .map_err(|e| e.to_string())
        .and_then(|n| {
            if n % 2 == 0 {
                Ok(n)
            } else {
                Err("number must be even".to_string())
            }
        })
}

The second stage may fail for domain reasons even after parsing succeeds. and_then expresses that sequence directly.

This is a useful reading pattern for Result::and_then: "If this succeeded, attempt the next fallible step."

The smell of nested wrappers

Nested wrappers are not always wrong, but they should make you stop and ask whether you are really modeling two distinct layers of meaning or whether you just need sequencing.

For example:

fn nested(input: Option<&str>) -> Option<Option<char>> {
    input.map(|s| s.trim().chars().next())
}

This type means:

  • outer None: there was no input at all
  • outer Some, inner None: there was input, but no character after trimming
  • outer Some, inner Some(c): there was input and a first character

That distinction may sometimes be useful.

But often the code does not need to preserve the difference between "missing input" and "present but empty after trimming." If the real question is just whether a character can be obtained, then and_then gives the simpler shape:

fn first_char(input: Option<&str>) -> Option<char> {
    input.and_then(|s| s.trim().chars().next())
}

The point is not that nesting is bad. The point is that nested wrappers should represent real meaning, not accidental structure.

Comparing `map` and `and_then` directly

A side-by-side comparison is often the fastest way to build intuition.

fn map_version(input: Option<&str>) -> Option<Option<usize>> {
    input.map(|s| s.split_once('=').map(|(_, value)| value.len()))
}
 
fn and_then_version(input: Option<&str>) -> Option<usize> {
    input.and_then(|s| s.split_once('=').map(|(_, value)| value.len()))
}

The closure returns Option<usize> because split_once('=') may fail.

That means:

  • map adds another layer: Option<Option<usize>>
  • and_then sequences the step: Option<usize>

The same pattern holds for Result:

fn map_result(input: Result<&str, String>) -> Result<Result<u32, String>, String> {
    input.map(|s| s.parse::<u32>().map_err(|e| e.to_string()))
}
 
fn and_then_result(input: Result<&str, String>) -> Result<u32, String> {
    input.and_then(|s| s.parse::<u32>().map_err(|e| e.to_string()))
}

Meaningful `Option` examples

Example 1: read the extension from an optional filename.

fn extension(input: Option<&str>) -> Option<&str> {
    input.and_then(|name| name.rsplit_once('.').map(|(_, ext)| ext))
}

Example 2: normalize optional input and parse it.

fn parse_timeout(input: Option<&str>) -> Option<u64> {
    input
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<u64>().ok())
}

Example 3: get the first path segment after trimming leading slashes.

fn first_segment(path: Option<&str>) -> Option<&str> {
    path.and_then(|p| p.trim_start_matches('/').split('/').next())
        .filter(|segment| !segment.is_empty())
}

Each example uses and_then at the point where the next step is also optional.

Meaningful `Result` examples

Example 1: parse then validate.

fn parse_percentage(input: &str) -> Result<u8, String> {
    input
        .trim()
        .parse::<u8>()
        .map_err(|e| e.to_string())
        .and_then(|n| {
            if n <= 100 {
                Ok(n)
            } else {
                Err("percentage must be between 0 and 100".to_string())
            }
        })
}

Example 2: parse a port and reject reserved values.

fn parse_port(input: &str) -> Result<u16, String> {
    input
        .trim()
        .parse::<u16>()
        .map_err(|e| e.to_string())
        .and_then(|port| {
            if port == 0 {
                Err("port must be non-zero".to_string())
            } else {
                Ok(port)
            }
        })
}

Example 3: look up then validate.

fn find_enabled<'a>(name: &str, enabled: &'a [String]) -> Result<&'a str, String> {
    enabled
        .iter()
        .find(|candidate| candidate.as_str() == name)
        .map(|s| s.as_str())
        .ok_or_else(|| format!("unknown name: {name}"))
        .and_then(|found| {
            if found.starts_with("x-") {
                Err("experimental names are not allowed here".to_string())
            } else {
                Ok(found)
            }
        })
}

In each case, the later step can also fail, which is why and_then is appropriate.

How `and_then` reads in practice

A helpful way to read and_then is to imagine the phrase "and then try this next step."

For example:

fn parse_level(input: Option<&str>) -> Option<u8> {
    input
        .map(str::trim)
        .and_then(|s| s.parse::<u8>().ok())
        .filter(|&n| n <= 5)
}

You can read it like this:

  • take the optional input
  • trim it if present
  • and then try parsing it
  • then keep it only if it is at most 5

That reading style is useful because it helps you distinguish structural roles:

  • map transforms
  • and_then sequences another wrapped step
  • filter keeps or discards based on a predicate

When `and_then` improves readability

and_then usually helps when all of the following are true:

  • the computation is genuinely sequential
  • each stage naturally returns Option or Result
  • the nested wrapper created by map would not carry independent meaning

This is especially common in parsing, lookup, validation, and configuration loading.

For example:

fn parse_env_number(raw: Option<&str>) -> Option<u32> {
    raw.and_then(|s| s.trim().parse::<u32>().ok())
}

This is clearer than a match because the whole operation is conceptually one optional pipeline.

When a `match` is better

Like map, and_then can become too dense if the closure starts doing the whole job.

For example:

fn classify(input: Option<&str>) -> Option<&'static str> {
    input.and_then(|s| {
        let s = s.trim();
        if s.is_empty() {
            None
        } else if s == "admin" {
            Some("privileged")
        } else if s == "guest" {
            Some("limited")
        } else {
            Some("standard")
        }
    })
}

This is valid, but the closure now contains normalization, rejection, and classification. That may be clearer as a helper function or explicit branching.

A good rule is this: and_then is excellent for connecting steps; it is less useful when one closure becomes a miniature subsystem.

A small CLI example

Here is a small example that uses and_then in a realistic command-line setting.

use std::env;
 
fn main() {
    let level = env::args()
        .nth(1)
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .and_then(|s| s.parse::<u8>().ok())
        .filter(|&n| n <= 5)
        .unwrap_or(3);
 
    println!("log level: {level}");
}

The interesting part is the parse step. Parsing yields Result<u8, _>, and .ok() converts it to Option<u8>. Because that transformation itself returns an Option, and_then keeps the chain flat.

You can try it with:

cargo run -- 4
cargo run -- " 5 "
cargo run -- nope
cargo run

A small project file for experimentation

You can experiment with the examples in a small project like this:

[package]
name = "chaining-computation-with-and-then"
version = "0.1.0"
edition = "2024"

Then place one example at a time in src/main.rs and run:

cargo run
cargo fmt
cargo clippy
cargo test

cargo clippy is especially useful here because it often reveals places where manual branching can be simplified or where a chain may be getting too dense.

Common mistakes

There are a few recurring mistakes around and_then.

First, using map when the closure already returns Option or Result, and then wondering why the type is nested.

Second, using and_then when the closure actually returns a plain value. In that case, map is the right tool.

Third, chaining so much logic into one closure that the flat structure becomes harder to understand than a small match or helper function.

Fourth, assuming nested wrappers are always a bug. Sometimes they represent meaningful distinctions, and flattening them would lose information.

The key is not to flatten everything. It is to flatten accidental structure when the computation is conceptually linear.

Refactoring patterns to watch for

When reviewing code, and_then often becomes relevant in these situations:

  1. a map closure returns Option<_> or Result<_, _>
  2. a match exists only to call the next optional or fallible step
  3. a chain becomes Option<Option<T>> or Result<Result<T, E>, E> without intentional meaning
  4. code conceptually reads as "if this works, try the next step"

A typical before-and-after looks like this:

fn before(input: Option<&str>) -> Option<Option<u32>> {
    input.map(|s| s.parse::<u32>().ok())
}
 
fn after(input: Option<&str>) -> Option<u32> {
    input.and_then(|s| s.parse::<u32>().ok())
}

That is the most common and_then refactoring in the early stages of Rust fluency.

Key takeaways

and_then is the standard tool for sequencing optional or fallible operations without building accidental nested structure.

The main ideas from this page are:

  • use map when the closure returns a plain value
  • use and_then when the closure returns Option or Result
  • and_then keeps linear optional or fallible computations flat
  • nested wrappers are sometimes meaningful, but often signal that sequencing is being expressed awkwardly
  • and_then is especially valuable in parsing, validation, lookup, and configuration code
  • when an and_then closure becomes the whole story, clearer structure may be better than a tighter chain

The next adjacent lesson is usually how to move from Option into Result cleanly, since many real computations start as optional lookups and become explicit error flows.