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"])