and_then, or_else, map_err mastery
Result combinators are methods on Result that allow functional-style error handling without explicit match statements. They enable chaining operations, transforming values, and handling errors elegantly.
// Without combinators (imperative)
let result = read_file();
let data = match result {
Ok(content) => content,
Err(e) => return Err(e),
};
// With combinators (functional)
let data = read_file()?;
fn parse_user_id(input: &str) -> Result<UserId, ParseError> {
input.parse::<u64>()
.map(UserId::new) // Transform u64 -> UserId
.map_err(|_| ParseError::InvalidUserId)
}
// Real-world: HTTP status to custom type
fn fetch_data() -> Result<Response, Error> {
http_get("/api/data")
.map(|body| Response { body, cached: false })
}
fn read_config() -> Result<Config, AppError> {
std::fs::read_to_string("config.toml")
.map_err(|e| AppError::ConfigRead {
path: "config.toml".into(),
source: e,
})
}
// Chain multiple map_err
fn process() -> Result<(), AppError> {
std::fs::read("data")
.map_err(AppError::from)? // io::Error -> AppError
}
fn get_user_email(user_id: u64) -> Result<String, DbError> {
find_user(user_id) // Result<User, DbError>
.and_then(|user| {
if user.email.is_empty() {
Err(DbError::NoEmail)
} else {
Ok(user.email)
}
})
}
// Real-world: database query chain
fn fetch_user_posts(user_id: u64) -> Result<Vec<Post>, Error> {
get_user(user_id)
.and_then(|user| get_posts_by_author(user.name))
.and_then(|posts| filter_published(posts))
}
fn read_config() -> Result<Config, Error> {
std::fs::read_to_string("config.toml")
.or_else(|_| std::fs::read_to_string("config.yaml"))
.or_else(|_| Ok(Config::default())) // Ultimate fallback
}
// Real-world: cache miss fallback
fn get_data(key: &str) -> Result<Data, Error> {
cache_get(key)
.or_else(|_| db_fetch(key).map(|data| {
cache_set(key, &data);
data
}))
}
// Static default
let value = config.get("timeout").unwrap_or(30);
// Computed default
let value = expensive_computation()
.unwrap_or_else(|_| {
log_error("Computation failed, using fallback");
fallback_value()
});
// Real-world: config with defaults
fn get_port() -> u16 {
std::env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8080)
}
use anyhow::{Context, Result};
async fn fetch_user_profile(user_id: u64) -> Result<UserProfile> {
http_get(&format!("/users/{}", user_id))
.await
.context("Failed to fetch user")?
.error_for_status()
.context("API returned error status")?
.json::<User>()
.await
.context("Failed to parse JSON")?
.try_into() // User -> UserProfile
.map_err(|e| anyhow!("Invalid profile data: {}", e))
}
// With combinator chain
async fn fetch_with_retry(url: &str) -> Result<Response> {
http_get(url)
.await
.or_else(|_| async {
tokio::time::sleep(Duration::from_secs(1)).await;
http_get(url).await
})
.or_else(|_| async {
tokio::time::sleep(Duration::from_secs(2)).await;
http_get(url).await
})
}
fn get_active_users() -> Result<Vec<UserSummary>, DbError> {
query("SELECT * FROM users WHERE active = true")
.fetch_all()
.and_then(|rows| {
rows.into_iter()
.map(|row| parse_user(&row))
.collect::<Result<Vec<_>, _>>()
})
.map(|users| {
users.into_iter()
.map(|u| UserSummary {
id: u.id,
name: u.name,
})
.collect()
})
}
// Transaction with rollback
fn transfer_funds(from: u64, to: u64, amount: f64) -> Result<(), DbError> {
begin_transaction()
.and_then(|tx| {
deduct_balance(&tx, from, amount)?;
add_balance(&tx, to, amount)?;
Ok(tx)
})
.and_then(|tx| tx.commit())
.or_else(|e| {
log_error("Transaction failed, rolling back");
Err(e)
})
}
fn find_user(id: u64) -> Result<User, Error> {
users.get(&id)
.cloned()
.ok_or(Error::NotFound)
}
// Lazy error creation
fn find_user_lazy(id: u64) -> Result<User, Error> {
users.get(&id)
.cloned()
.ok_or_else(|| Error::NotFound {
resource: "User".into(),
id
})
}
// Option<Result<T, E>> -> Result<Option<T>, E>
let result: Result<Option<Data>, Error> =
optional_fetch() // Option<Result<Data, Error>>
.transpose();
// Real-world: optional config field
fn load_optional_feature() -> Result<(), Error> {
config.get("feature_flag")
.map(|flag| parse_feature(flag))
.transpose()?; // Propagate parse error if Some
Ok(())
}
// Result<Result<T, E>, E> -> Result<T, E>
let result: Result<Data, Error> = outer_operation()
.map(|inner_result| inner_result)
.flatten();
fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, ParseError> {
inputs.iter()
.map(|s| s.parse::<i32>())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| ParseError::InvalidNumber)
}
// Short-circuit on first error
let numbers: Result<Vec<_>, _> =
strings.iter().map(|s| s.parse()).collect();
let results: Vec<Result<Data, Error>> = process_batch();
let (successes, errors): (Vec<_>, Vec<_>) =
results.into_iter()
.partition(Result::is_ok);
let successes: Vec<Data> =
successes.into_iter().map(Result::unwrap).collect();
let errors: Vec<Error> =
errors.into_iter().map(Result::unwrap_err).collect();
fn process_user_data(user_id: u64) -> Result<Report, Error> {
fetch_user(user_id)
.and_then(validate_user)
.and_then(fetch_user_activity)
.map(calculate_metrics)
.and_then(generate_report)
.map_err(|e| Error::ProcessingFailed {
user_id,
source: Box::new(e)
})
}
fn validate_and_process(input: Input) -> Result<Output, Error> {
let validated = validate(input)?;
let processed = process(validated)?;
let finalized = finalize(processed)?;
Ok(finalized)
}
// Same with combinators
fn validate_and_process(input: Input) -> Result<Output, Error> {
validate(input)
.and_then(process)
.and_then(finalize)
}
| Combinator | Use When | Example |
|------------|----------|---------|
| map | Transform success value | Convert types |
| map_err | Transform error | Add context |
| and_then | Chain operations | Sequential queries |
| or_else | Provide fallback | Cache miss -> DB |
| unwrap_or | Default value | Config defaults |
| ok_or | Option -> Result | Collection lookup |
// BAD: Panics on error
let value = dangerous_operation().unwrap();
// GOOD: Handle error
let value = dangerous_operation()
.unwrap_or_else(|e| {
log_error(e);
default_value()
});
// BAD: Silently ignores error
let _ = save_to_file(data);
// GOOD: At least log
save_to_file(data)
.unwrap_or_else(|e| log_error("Save failed: {}", e));
// BAD: Hard to read
result.map(|x| x.map(|y| y.and_then(|z| process(z))))
// GOOD: Extract functions
result.and_then(process_nested)
fn process_nested(x: X) -> Result<Z, Error> {
x.map(|y| y.and_then(process))
}
Create a config parser using only combinators (no match).
Hints:Implement retry with exponential backoff using combinators.
Hints:Process items in batch, collect successes and errors separately.
Hints:Extensive use of combinators for parsing.
View on GitHubAsync combinators for futures.
View on GitHubHTTP client with combinator chains.
View on GitHubRun this code in the official Rust Playground