Error Handling

Result and Option Combinators

Combinators on Option<T> and Result<T, E> are method adapters that filter, combine, inspect, flatten, or convert between optional and fallible values without writing multi-step match expressions.

Combinator Reference

Category Method Signature Conversion
Option -> Result opt.ok_or(err) / opt.ok_or_else(f) Option<T> -> Result<T, E>
Result -> Option res.ok() / res.err() Result<T, E> -> Option<T> / Option<E>
Transposition opt.transpose() / res.transpose() Option<Result<T, E>> <-> Result<Option<T>, E>
Filtering opt.filter(predicate) Option<T> -> Option<T> (None if predicate is false)
Pairing opt_a.zip(opt_b) (Option<A>, Option<B>) -> Option<(A, B)>
Boolean Algebra a.and(b) / a.or(b) / a.xor(b) Combines two Option or Result values
Side-Effect Inspection inspect(f) / inspect_err(f) Calls f(&v) or f(&e) for logging and returns self unchanged
Flattening nested.flatten() Option<Option<T>> -> Option<T>

Bridging Between Option<T> and Result<T, E>

Option<T> ── .ok_or_else(|| err) ──> Result<T, E>
Result<T, E> ── .ok()            ──> Option<T>      (discards Err)

Option<Result<T, E>> <── .transpose() ──> Result<Option<T>, E>

transpose() is especially useful when an optional field (such as Option<&str>) needs to be parsed with a fallible function (str::parse), turning Option<Result<T, E>> into Result<Option<T>, E> so ? can propagate a parse error while preserving None:

use std::num::ParseIntError;

fn parse_optional_port(raw: Option<&str>) -> Result<Option<u16>, ParseIntError> {
    raw.map(|s| s.trim().parse::<u16>()).transpose()
}

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

Filtering and Zipping Option Values

  • filter(|v| ...) keeps Some(v) if the predicate returns true, or turns it into None.
  • zip(other) pairs Some(a) and Some(b) into Some((a, b)), returning None if either side is None.
fn build_coordinates(lat: Option<f64>, lon: Option<f64>) -> Option<(f64, f64)> {
    let valid_lat = lat.filter(|&y| (-90.0..=90.0).contains(&y));
    let valid_lon = lon.filter(|&x| (-180.0..=180.0).contains(&x));
    valid_lat.zip(valid_lon)
}

fn main() {
    println!("Valid pair:   {:?}", build_coordinates(Some(23.62), Some(90.50)));
    println!("Out of range: {:?}", build_coordinates(Some(120.0), Some(90.50)));
    println!("Missing lon:  {:?}", build_coordinates(Some(23.62), None));
}
Valid pair:   Some((23.62, 90.5))
Out of range: None
Missing lon:  None

Observing Values in Place with inspect() and inspect_err()

inspect() and inspect_err() take a shared reference (&T or &E) to log, trace, or assert on intermediate pipeline values without consuming or altering the Option or Result:

fn process_score(raw: &str) -> Result<u32, std::num::ParseIntError> {
    raw.trim()
        .parse::<u32>()
        .inspect(|val| println!("  [trace] parsed raw score: {val}"))
        .map(|val| val * 10)
        .inspect_err(|err| println!("  [error] failed to parse {raw:?}: {err}"))
}

fn main() {
    let _ = process_score(" 15 ");
    let _ = process_score("invalid");
}
  [trace] parsed raw score: 15
  [error] failed to parse "invalid": invalid digit found in string