In this lesson, you will learn how to chain multiple fallible operations together using and_then and or_else to create clean, readable error-handling flows.
The Problem with Nesting
When you perform a series of operations that might fail, you often end up with deeply nested match statements. This "pyramid of doom" makes your code hard to follow. Combinators allow us to treat the Result as a pipeline where data flows through transformations only if every step succeeds.
Chaining Success with and_then
The and_then combinator is used when you have a function that returns a Result and you want to use that result to perform another operation that also returns a Result. Think of it as a "flat map": if the first operation succeeds, and_then unwraps the value and passes it to the next function. If it fails, it skips the rest and propagates the error.
fn get_user_id(username: &str) -> Result<u32, &str> { /* ... */ }
fn fetch_data(id: u32) -> Result<String, &str> { /* ... */ }
// Chain them together cleanly
let data = get_user_id("alice")
.and_then(fetch_data);
Providing Fallbacks with or_else
While and_then handles the "happy path," or_else handles the "recovery path." If your operation fails, or_else triggers a function that allows you to provide a fallback Result. This is perfect for when a primary source fails and you have a secondary source to try instead.
let config = read_primary_config()
.or_else(|_| read_backup_config())
.or_else(|_| Ok(default_config()));
Why This Matters
By using these combinators, you replace verbose match blocks with a declarative "pipeline" style. Your logic reads like a sequence of steps rather than a tree of branches. You define the flow of execution, and Rust’s Result type manages the short-circuiting logic for you—stopping automatically the moment an error occurs.
Key Takeaway: Use and_then to chain dependent operations that might fail, and use or_else to define fallback logic, keeping your error handling concise and flat.