When one item becomes many
You're processing a log file. Each line can contain multiple error codes separated by commas. You need a flat list of every error code across the entire file. You reach for map to split each line into a vector of codes. The compiler accepts this, but now you have a Vec<Vec<Code>>. You need to flatten that nested structure. You add .flatten() to the chain. It works, but you just allocated a new vector for every single line. You're creating garbage just to throw it away.
flat_map solves this by combining the transformation and the flattening into one step. It yields items directly from the inner iterators without building intermediate collections. You get the same result as map followed by flatten, but with zero extra allocations and better performance.
The concept: pouring onto the conveyor
map takes an item, transforms it, and puts the result into a package. flat_map takes an item, transforms it, and pours the result directly onto the output conveyor.
If the transformation produces multiple items, they all go straight onto the conveyor. If it produces nothing, the conveyor keeps moving. The critical difference is the package. map always produces a package. flat_map discards the package and keeps only the contents.
In Rust terms, flat_map expects your closure to return something that implements IntoIterator. This includes vectors, slices, arrays, Option, Result, and custom iterators. flat_map consumes that inner iterable and yields its items one by one.
Minimal example
/// Extracts all words from a list of sentences.
fn main() {
let sentences = vec![
"Rust is safe",
"and fast",
];
// flat_map applies the closure to each sentence.
// The closure returns an iterator of words.
// flat_map yields each word directly to the output stream.
let words: Vec<&str> = sentences
.iter()
.flat_map(|s| s.split_whitespace())
.collect();
assert_eq!(words, vec!["Rust", "is", "safe", "and", "fast"]);
}
The closure returns split_whitespace(), which is an iterator. flat_map grabs that iterator and pulls items from it until it's exhausted. Only then does it move to the next sentence. No vectors are created. The Vec<&str> at the end holds references to the original strings, which is efficient.
How it works under the hood
flat_map is a combinator. It returns a FlatMap iterator, which is a state machine holding two pieces of state: the source iterator and the current inner iterator.
When you call next() on the FlatMap, it checks the inner iterator first. If the inner iterator has items, it yields the next one. If the inner iterator is exhausted, FlatMap asks the source iterator for the next item. It runs your closure on that item to get a new inner iterator, stores it, and repeats the check.
This lazy evaluation means flat_map does no work until you start consuming the iterator. You can chain flat_map with filter, take, or skip without processing the entire dataset. If you call take(1) after flat_map, the source iterator yields only one item, the closure runs once, and the first item from the inner iterator is returned. The rest of the data is never touched.
Laziness is the default. Pay only for what you consume.
Realistic example: filtering with Option
A common pattern is transforming items while discarding some based on a condition. You can use map followed by filter, but that requires two passes over the logic. flat_map with Option does both in one step.
Option implements IntoIterator. Some(value) yields one item. None yields zero items. When you pass an Option to flat_map, it automatically filters out the None values.
/// Represents a user and their optional permissions.
struct User {
id: u32,
permissions: Vec<String>,
}
/// Parses a permission string, returning None if it's empty or invalid.
fn parse_permission(s: &str) -> Option<String> {
let trimmed = s.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
}
fn main() {
let users = vec![
User { id: 1, permissions: vec!["read".to_string(), " ".to_string(), "#admin".to_string()] },
User { id: 2, permissions: vec!["write".to_string()] },
];
// flat_map with Option filters out None values automatically.
// This combines mapping and filtering in a single pass.
let valid_permissions: Vec<String> = users
.iter()
.flat_map(|user| user.permissions.iter().filter_map(|p| parse_permission(p)))
.collect();
assert_eq!(valid_permissions, vec!["read", "write"]);
}
The inner filter_map handles the parsing logic for each permission string. The outer flat_map flattens the permissions across all users. This structure is readable and efficient.
Convention aside: Using flat_map with Option is the idiomatic way to map and filter simultaneously. It's preferred over map(...).filter_map(...) because it reduces nesting and makes the intent clear. The community recognizes this pattern immediately.
Pitfalls and compiler errors
If your closure returns a single value like String, the compiler rejects it with E0277 (trait bound not satisfied). String does not implement IntoIterator. You must return an iterator, a collection, a slice, or an Option.
// This fails to compile.
let bad: Vec<String> = vec!["a", "b"]
.iter()
.flat_map(|s| s.to_string()) // E0277: String is not IntoIterator
.collect();
The fix is to wrap the value in an iterator. Use std::iter::once(value) to yield a single item, or return Some(value) if you're using the Option idiom.
If your closure returns an iterator that borrows the input, you can't collect the result into a Vec of owned types. The compiler will complain about references escaping the closure scope. This usually manifests as E0515 (cannot return value referencing local variable).
// This fails to compile.
let bad: Vec<String> = vec!["hello"]
.iter()
.flat_map(|s| s.split_whitespace().map(|w| w.to_string())) // E0515: closure returns iterator borrowing s
.collect();
The error occurs because the FlatMap iterator holds a reference to the inner iterator, which borrows s. When s goes out of scope at the end of the closure, the reference becomes dangling. The compiler prevents this. The fix is to ensure the inner iterator owns its data, or collect into a Vec<&T> if the lifetimes allow.
Another trap is returning a Vec when you meant to return an iterator. The code compiles, but you're allocating memory for every item. The compiler won't stop you, but your profiler will. Always return an iterator or a slice when possible. Avoid Vec inside flat_map closures unless you have no other choice.
Return an iterator, not a vector. Save the allocation.
Decision matrix
Use flat_map when each input item expands into a collection of output items, like splitting sentences into words or expanding a list of files into their contents.
Use flat_map with Option when you want to transform items and discard None results in a single pass, avoiding a separate filter step.
Use filter_map when the transformation produces at most one output item per input. filter_map signals intent more clearly than flat_map for optional results.
Use map followed by flatten only when the mapping logic is so complex that extracting it into a named function improves readability, and that function returns a collection. Even then, flat_map with the function call is usually cleaner.
Use flat_map over map + collect + extend when you want to avoid intermediate allocations. flat_map streams items directly to the destination.
Pick the combinator that matches the shape of your data. One-to-many gets flat_map. One-to-zero-or-one gets filter_map.