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

Combining Multiple Fallible Operations

Real-world functions frequently combine multiple Option<T> and Result<T, E> operations. Rust provides combinators (zip, and_then, or_else), tuple pattern matching, and the ? operator to compose multiple fallible values cleanly.

1. Pairing Two Optional Values with Option::zip and zip_with

When two independent Option values must both be Some to proceed, a.zip(b) returns Some((x, y)) if both are Some, or None if either is None:

fn format_full_name(first: Option<&str>, last: Option<&str>) -> Option<String> {
    first
        .zip(last)
        .map(|(f, l)| format!("{f} {l}"))
}

fn main() {
    println!("Both present: {:?}", format_full_name(Some("Ada"), Some("Lovelace")));
    println!("One missing:  {:?}", format_full_name(Some("Ada"), None));
}
Both present: Some("Ada Lovelace")
One missing:  None

2. Selecting the First Available Fallback (or and or_else)

When resolving a setting from a priority chain (such as CLI flag $\rightarrow$ environment variable $\rightarrow$ config file $\rightarrow$ default), chain .or() (for already-evaluated Options) and .or_else() (for lazy lookups):

fn resolve_region(
    cli_flag: Option<&str>,
    env_var: impl FnOnce() -> Option<&'static str>,
) -> &'static str {
    cli_flag
        .and_then(|s| if s == "eu" { Some("eu-central-1") } else { None })
        .or_else(env_var)
        .unwrap_or("us-east-1")
}

fn main() {
    println!("CLI override: {}", resolve_region(Some("eu"), || Some("ap-south-1")));
    println!("Env fallback: {}", resolve_region(None, || Some("ap-south-1")));
    println!("Default:      {}", resolve_region(None, || None));
}
CLI override: eu-central-1
Env fallback: ap-south-1
Default:      us-east-1

3. Accumulating All Validation Errors Instead of Short-Circuiting

While the ? operator stops at the first error, user-facing form or config validators often want to run all checks and return a list of every validation failure (Result<Config, Vec<String>>):

#[derive(Debug)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
}

fn validate_config(
    raw_host: &str,
    raw_port: &str,
    raw_workers: &str,
) -> Result<ServerConfig, Vec<String>> {
    let mut errors = Vec::new();

    let host = if raw_host.trim().is_empty() {
        errors.push(String::from("host must not be empty"));
        String::new()
    } else {
        raw_host.trim().to_owned()
    };

    let port = match raw_port.parse::<u16>() {
        Ok(p) if p >= 1024 => p,
        Ok(p) => {
            errors.push(format!("port {p} must be >= 1024"));
            0
        }
        Err(e) => {
            errors.push(format!("invalid port '{raw_port}': {e}"));
            0
        }
    };

    let workers = match raw_workers.parse::<usize>() {
        Ok(w) if w > 0 => w,
        _ => {
            errors.push(format!("workers '{raw_workers}' must be a positive integer"));
            0
        }
    };

    if errors.is_empty() {
        Ok(ServerConfig {
            host,
            port,
            workers,
        })
    } else {
        Err(errors)
    }
}

fn main() {
    println!("Valid:   {:?}", validate_config("localhost", "8080", "4"));
    println!("Invalid: {:?}", validate_config("  ", "80", "0"));
}
Valid:   Ok(ServerConfig { host: "localhost", port: 8080, workers: 4 })
Invalid: Err(["host must not be empty", "port 80 must be >= 1024", "workers '0' must be a positive integer"])