14.5 and_then: Chaining Fallible Operations
Right, so you’ve got the basics of Result and ? down. You can handle a single fallible operation gracefully. But let’s be real, software is rarely that simple. You’re almost always dealing with a chain of operations where any one of them could fail. You could write a whole mess of match statements, but we’re better than that. You could use ? a bunch, but that only works if you’re returning the same error type all the way up, which, as we’ll see, isn’t always the case.
This is where and_then enters the picture, looking all sophisticated and ready to save the day. It’s the monadic bind operation for the Result type, but don’t let that term scare you. In plain English, it’s your tool for saying, “Do this next thing, but only if the last thing worked. And oh, that next thing might also fail.”
Think of it like this: you’re trying to make a cake. The steps are: preheat oven -> mix ingredients -> bake -> frost. If “preheat oven” fails (the element is broken), there’s no point in even attempting to “mix ingredients.” and_then is the workflow manager for this exact scenario.
How and_then Actually Works
The signature is the key to understanding it:
fn and_then<U, F>(self, op: F) -> Result<U, E>
where
F: FnOnce(T) -> Result<U, E>,
Let’s translate from Rust-ese. It says: “I, a Result<T, E>, will take a closure op. This closure must be a function that takes the success value inside me (T) and returns a new Result<U, E>.”
The magic is in the execution:
- If I (
self) am anErr(e), I just returnErr(e)immediately. I don’t even bother calling your closure. - If I am an
Ok(t), I take that valuetand pass it to your closureop. WhateverResult<U, E>your closure returns becomes my final result.
Here’s the classic example: parsing a string into a number and then performing a fallible operation on that number.
fn double_first(string: &str) -> Result<i32, String> {
string.parse::<i32>()
.map_err(|e| e.to_string()) // Convert the parse error to a String
.and_then(|num| {
if num % 2 == 0 {
Ok(num * 2)
} else {
Err(String::from("number was not even"))
}
})
}
fn main() {
println!("{:?}", double_first("10")); // Ok(20)
println!("{:?}", double_first("5")); // Err("number was not even")
println!("{:?}", double_first!("not a number")); // Err("invalid digit found in string")
}
Notice how the error types have to align. The closure passed to and_then must return a Result<_, String> because the initial parse was converted to that error type. This brings us to the single biggest headache with and_then.
The Error Type Mismatch Problem
This is the part the cheerful tutorials often gloss over. and_then is brutally strict about error types. The closure must return a Result with the exact same E (error type) as the Result you’re calling it on.
Look what happens if we don’t manually convert the error in the previous example:
fn double_first_broken(string: &str) -> Result<i32, std::num::ParseIntError> {
string.parse::<i32>()
.and_then(|num| { // This won't compile!
if num % 2 == 0 {
Ok(num * 2)
} else {
Err(String::from("number was not even")) // Error type is String, not ParseIntError!
}
})
}
The compiler will stop you dead in your tracks. The initial parse() returns a Result<i32, ParseIntError>, but our closure is trying to return a Result<i32, String>. These are two completely different Result types. Mismatch. Failure.
This is the universe’s way of forcing you to be explicit about your error handling. You have a few ways to solve this, and the best choice depends on your context:
- Convert Errors Manually: As we did first with
map_err(|e| e.to_string()). It works, but it’s a bit tedious and can lose type information. - Use a Boxed Error Trait Object: Return
Result<U, Box<dyn std::error::Error>>. This is more ergonomic for quick scripts but loses type safety. - Use a Proper Error Enum (The Right Way): This is what you should be doing in serious code. Define an enum that can represent all the possible errors in your chain.
#[derive(Debug)]
enum DoubleError {
Parse(std::num::ParseIntError),
NotEven,
}
fn double_first_proper(string: &str) -> Result<i32, DoubleError> {
string.parse::<i32>()
.map_err(DoubleError::Parse) // Convert ParseIntError into our custom enum variant
.and_then(|num| {
if num % 2 == 0 {
Ok(num * 2)
} else {
Err(DoubleError::NotEven) // Use our other variant
}
})
}
Now the error type is consistently DoubleError throughout the entire chain. This is clean, explicit, and type-safe. The caller can match on the exact reason for failure.
and_then vs. The Almighty ?
This is a common point of confusion. When do you use which? They are complementary tools.
- Use
?when you are inside a function that returns aResultand you want to propagate an error upwards. It’s for early returns. It’s fantastic for flattening a series of operations where you don’t need to process theOkvalue in between. - Use
and_thenwhen you want to chain operations together where each subsequent operation requires the success value of the previous one and might itself fail. It’s a way to build a pipeline of fallible functions.
You can absolutely use them together. In fact, it’s often the most elegant solution. Let’s rewrite our proper example using ? inside the closure for and_then:
fn get_user_id(name: &str) -> Result<Option<i32>, String> { /* ... */ }
fn get_user_profile(id: i32) -> Result<Profile, String> { /* ... */ }
// We want to find a user by name and get their profile, handling the case where they might not exist.
fn find_profile(name: &str) -> Result<Option<Profile>, String> {
get_user_id(name)
.and_then(|maybe_id| {
// Use `?` to propagate a `Err(String)` from inside this closure.
// If `maybe_id` is None, we map it to a success value of `Ok(None)`.
match maybe_id {
Some(id) => get_user_profile(id).map(Some),
None => Ok(None),
}
})
}
Here, and_then is managing the overall chain from get_user_id to the next step, and ? (or in this case, a manual match which could be replaced with combinators like ok_or_else and ?) is handling the propagation within that step. This is the power move. This is how you write robust, clear error-handling code that doesn’t make you want to gouge your eyes out.