Option<T> and Result<T, E> in Depth

transpose()

The .transpose() method swaps the outer and inner layers of a nested Option<Result<T, E>> and Result<Option<T>, E>, allowing you to use the ? error-propagation operator on optional fallible operations.

Signatures and Mapping Table

impl<T, E> Option<Result<T, E>> {
    pub fn transpose(self) -> Result<Option<T>, E>;
}

impl<T, E> Result<Option<T>, E> {
    pub fn transpose(self) -> Option<Result<T, E>>;
}
Option<Result<T, E>> $\Longleftrightarrow$ Result<Option<T>, E>
None $\Longleftrightarrow$ Ok(None)
Some(Ok(v)) $\Longleftrightarrow$ Ok(Some(v))
Some(Err(e)) $\Longleftrightarrow$ Err(e)

Parsing an Optional Configuration Value (Option<&str> -> Result<Option<T>, E>)

A very common scenario is parsing an optional setting (such as an environment variable or CLI flag Option<&str>):

  • If the setting is None, the result should be Ok(None) (not an error!).
  • If the setting is Some(raw), parsing raw.parse::<T>() returns Result<T, E>, so opt.map(str::parse) produces Option<Result<T, E>>.
  • Calling .transpose() turns Option<Result<T, E>> into Result<Option<T>, E>, which can then be unwrapped with ?:
use std::num::ParseIntError;

fn parse_optional_port(raw_port: Option<&str>) -> Result<Option<u16>, ParseIntError> {
    // 1. `raw_port.map(str::parse)` has type `Option<Result<u16, ParseIntError>>`
    // 2. `.transpose()` converts it to `Result<Option<u16>, ParseIntError>`
    raw_port.map(str::parse::<u16>).transpose()
}

fn main() {
    println!("None:        {:?}", parse_optional_port(None));
    println!("Some('443'): {:?}", parse_optional_port(Some("443")));
    println!("Some('bad'): {:?}", parse_optional_port(Some("bad")));
}
None:        Ok(None)
Some('443'): Ok(Some(443))
Some('bad'): Err(ParseIntError { kind: InvalidDigit })

Bridging Result<Option<T>, E> into an Iterator Pipeline (filter_map)

Conversely, when an iterator step (such as .filter_map(...)) expects an Option<Result<T, E>> (None to skip the item, Some(Ok(v)) to keep it, or Some(Err(e)) to propagate the error), calling .transpose() on a Result<Option<T>, E> flips it into the required Option<Result<T, E>>:

use std::num::ParseIntError;

fn parse_non_comment_line(line: &str) -> Result<Option<i32>, ParseIntError> {
    let trimmed = line.trim();
    if trimmed.is_empty() || trimmed.starts_with('#') {
        Ok(None)
    } else {
        Ok(Some(trimmed.parse::<i32>()?))
    }
}

fn main() {
    let lines = ["# header", "10", "", "25", "# footer"];

    // `.transpose()` turns `Result<Option<i32>, E>` -> `Option<Result<i32, E>>` for `filter_map`:
    let parsed: Result<Vec<i32>, ParseIntError> = lines
        .iter()
        .filter_map(|line| parse_non_comment_line(line).transpose())
        .collect();

    println!("Parsed numbers: {parsed:?}");
}
Parsed numbers: Ok([10, 25])