When the assembly line hits a defect
You are processing a batch of user uploads. Each file needs validation and transformation. One file is corrupted. What happens? If you panic, the whole batch dies and the user gets a generic crash. If you skip the file silently, the user might think it succeeded when it didn't. If you stop at the first error, you might miss valid files later in the queue that could have been processed.
Iterator chains are excellent for transforming data. They compose cleanly and run efficiently. They also assume every item is valid. When errors enter the stream, the chain breaks unless you handle them explicitly. The Iterator trait does not know about Result. It only knows how to pass items down the belt. You have to program the behavior for defects.
Iterators are dumb pipes
Think of an iterator chain as an assembly line. The Iterator trait defines a single method: next(). It yields Option<T>. The belt moves items forward. If next() returns Some(item), the item flows to the next stage. If it returns None, the belt stops.
The belt does not inspect the contents of the boxes. If your transformation function returns a Result<T, E>, the iterator sees that Result as just another type. The Result becomes the item flowing down the chain. The chain does not stop for errors. It does not skip errors. It passes the Result wrapper along until you intervene.
You have to decide what to do when a defect appears. Do you stop the line? Do you toss the defective box and keep moving? Do you collect all the defects and report them at the end? Rust gives you tools for every strategy. You choose based on the semantics of your data.
Fail fast with collect into Result
The most common pattern is to process everything and fail if any item fails. This is the "all or nothing" strategy. You want a Vec<T> if everything succeeds, or an E if anything fails.
Rust implements FromIterator<Result<T, E>> for Result<Vec<T>, E>. This implementation is the key. When you call collect() and the target type is Result<Vec<T>, E>, Rust uses this trait. The implementation iterates through the items, pushes Ok values into a vector, and the moment it encounters an Err, it stops immediately and returns that error. It short-circuits.
let inputs = vec!["10", "20", "not_a_number", "40"];
// Map returns Result. Collect gathers them into a single Result<Vec, E>.
// The type annotation tells Rust to use the FromIterator impl for Result.
let results: Result<Vec<i32>, _> = inputs
.iter()
.map(|s| s.parse::<i32>()) // parse returns Result<i32, ParseIntError>
.collect();
match results {
Ok(nums) => println!("All parsed: {:?}", nums),
Err(e) => println!("First error: {}", e),
}
The map closure returns Result<i32, ParseIntError>. The collect call sees the target type Result<Vec<i32>, _> and activates the short-circuiting logic. It processes "10" and "20", pushes them to the vector, hits "not_a_number", sees the Err, and returns immediately. It never looks at "40".
If you omit the type annotation, Rust often guesses wrong. It might infer Vec<Result<i32, ParseIntError>>. That is a vector of results, not a result of a vector. You end up with a list containing Ok(10), Ok(20), Err(...), Ok(40). That is rarely what you want. The compiler will reject this with E0277 (trait bound not satisfied) if you try to use the vector as a list of integers.
Short-circuiting saves work, but it also hides downstream errors. Know the trade-off.
Skip the bad with filter_map
Sometimes the data source is noisy. You have a configuration file with comments and blank lines. You have a log stream with malformed entries. You want to extract the valid data and ignore the rest. Panicking is wrong. Stopping is wrong. You need to skip.
Use filter_map for this. The closure returns Option<T>. Some(value) passes the item to the next stage. None drops the item. The chain continues processing remaining elements without interruption.
let lines = vec!["host=localhost", "# comment", "port=abc", "port=8080"];
let config: Vec<String> = lines
.into_iter()
.filter_map(|line| {
// Skip comments and blank lines
if line.starts_with('#') || line.is_empty() {
return None;
}
// Try to parse the port. If it fails, skip the line.
// We only care about valid ports for this example.
if line.starts_with("port=") {
let port_str = &line[5..];
if let Ok(port) = port_str.parse::<u16>() {
return Some(format!("port={}", port));
}
}
None
})
.collect();
println!("Valid config: {:?}", config);
The filter_map closure inspects each line. Comments return None. Lines that don't match the pattern return None. Lines with invalid ports return None. Only valid port lines return Some. The result is a clean vector of configuration strings.
The community prefers filter_map over chaining filter and map. A filter followed by a map requires two closures and two passes over the logic, even though iterators are lazy. filter_map combines the decision and the transformation into one closure. It is more concise and often faster because the compiler can optimize the single closure better.
Silent skipping is a feature, not a bug, when the data source is noisy.
Collect everything with fold
Short-circuiting hides errors. Skipping hides errors. Sometimes you need to report all failures. You are validating a batch of records. You want to process the valid ones and return a list of all the invalid ones so the user can fix them.
Use fold for this. fold lets you accumulate any state you want. You can build a tuple of successes and errors. You can build a custom struct. You have total control.
let inputs = vec!["10", "bad", "30", "also_bad", "50"];
// Fold accumulates everything. We build a tuple of (successes, errors).
let (successes, errors): (Vec<i32>, Vec<String>) = inputs
.into_iter()
.fold((Vec::new(), Vec::new()), |(mut ok, mut err), item| {
match item.parse::<i32>() {
Ok(n) => ok.push(n),
Err(e) => err.push(e.to_string()),
}
(ok, err)
});
println!("Successes: {:?}", successes);
println!("Errors: {:?}", errors);
The fold closure receives the accumulator (ok, err) and the current item. It parses the item. If it succeeds, it pushes to ok. If it fails, it pushes the error message to err. It returns the updated tuple. The chain processes every item. Nothing is skipped. Nothing is hidden.
There is also try_fold. It works like fold but supports short-circuiting. If the closure returns Err, try_fold stops and returns that error. If it returns Ok, it continues. try_fold is useful when you need custom state during accumulation but still want to abort on a fatal error. For collecting all errors, plain fold is the right tool.
Fold gives you total control. Build the state machine you need.
Pitfalls and compiler errors
Iterator error handling trips up developers in predictable ways. Watch for these patterns.
Type inference hell. The collect method is notoriously picky about types. When you collect Result items, Rust needs to know whether you want Result<Vec<T>, E> or Vec<Result<T, E>>. Without a hint, it often guesses the latter. You get a vector of results instead of a result of a vector. The compiler rejects this with E0277 (trait bound not satisfied) when you try to use the vector as values. Always annotate the type on collect when dealing with Result or Option.
// BAD: Rust infers Vec<Result<i32, _>>. You get a list of results.
let bad: Vec<_> = inputs.iter().map(|s| s.parse::<i32>()).collect();
// GOOD: Explicit type forces Result<Vec<i32>, _>.
let good: Result<Vec<i32>, _> = inputs.iter().map(|s| s.parse::<i32>()).collect();
Unwrap inside iterators. Never call unwrap() or expect() inside a map or filter closure unless you are absolutely certain the value is valid. If the value is invalid, the iterator panics mid-chain. The panic unwinds the stack and kills the thread. You lose the rest of the batch. Use filter_map to skip, or collect into Result to propagate. If you must panic, do it outside the iterator with a clear message.
Mismatched types in filter_map. The filter_map closure must return Option<T> where T matches the output type of the chain. If you return Option<String> but the chain expects i32, the compiler rejects you with E0308 (mismatched types). Check the return type of your closure against the target type of collect.
Convention aside: The community convention for parse is to use it directly in map when collecting into Result. Do not wrap it in Ok manually. map(|s| s.parse()) is correct. map(|s| Ok(s.parse())) creates a Result<Result<T, E>, F>, which is a nested result and usually a mistake.
Annotate the type on collect. The compiler needs a nudge, and your future self will thank you.
Decision matrix
Choose the error handling strategy based on the failure mode of your data.
Use collect into Result<Vec<T>, E> when the operation must succeed for all items or fail entirely. The first error aborts the chain and propagates up. This is the standard pattern for batch operations where partial success is unacceptable.
Use filter_map with Option when invalid items should be silently skipped. The chain continues processing remaining elements without interruption. This is the right choice for noisy data sources like logs, config files, or user input where some entries are expected to be malformed.
Use fold with a tuple or struct when you need to accumulate all errors instead of stopping at the first one. You can collect successes and failures together and report them at the end. This is essential for validation workflows where the user needs to fix all issues at once.
Use try_fold when you need custom state during accumulation but still want to abort on a fatal error. You can maintain counters, flags, or partial results while short-circuiting on critical failures. This is rare but powerful for complex pipelines.
Use a plain for loop when the iterator abstractions become too complex. If you need branching logic that depends on previous items, or if the error handling requires side effects that iterators cannot express cleanly, drop back to a loop. Iterators are expressive, but they are not magic.
Match the tool to the failure mode. Don't force a skip strategy on a critical operation.