The box of boxes problem
You have a list of shopping lists. Each person on your team submitted their own list of items. In your code, this looks like a Vec<Vec<String>>. You need to merge them all into one master list to check off items at the store. Or you have a game board split into zones, and you want to iterate over every tile without caring which zone it belongs to. Or you are parsing a config file where each section has its own list of keys, and you want to find a key regardless of section.
The data is nested. You need it flat.
Writing a loop to push items one by one works, but it feels manual. You have to manage indices or nested loops. You risk off-by-one errors. Rust gives you a cleaner tool: the flatten iterator adapter. It turns a stream of streams into a single stream. You chain it between your data and the code that consumes it.
How flatten works
flatten is an iterator adapter. It does not allocate memory. It does not copy data. It returns a new iterator struct that wraps your original iterator. When you ask this new iterator for the next item, it reaches into the inner iterators and pulls items out one by one.
Think of a conveyor belt carrying boxes. Each box contains smaller packages. flatten is a robot arm that grabs a box, opens it, and places the packages onto the main belt one at a time. When the box is empty, the robot grabs the next box. The output is a single stream of packages.
The adapter relies on the IntoIterator trait. The items produced by the outer iterator must themselves be iterable. A Vec<T> implements IntoIterator, so Vec<Vec<T>> works. A &Vec<T> also implements IntoIterator, so borrowing works too. Even Option<T> and Result<T, E> implement IntoIterator, which leads to some surprising behavior later.
fn main() {
// Nested structure: a vector of vectors.
let nested: Vec<Vec<i32>> = vec![vec![1, 2], vec![3, 4], vec![5]];
// iter() borrows the outer vector.
// flatten() yields references to the inner items.
// copied() converts &i32 to i32 because i32 implements Copy.
// collect() gathers the stream into a new owned vector.
let flat: Vec<i32> = nested.iter().flatten().copied().collect();
println!("{:?}", flat); // [1, 2, 3, 4, 5]
}
The flatten adapter is lazy. Calling flatten does nothing until you drive the iterator. collect drives the iterator by repeatedly calling next. This means you can combine flatten with other adapters like filter or map without creating intermediate collections. The work happens in a single pass over the data.
Trust the iterator. It only does work when you ask for results.
Borrowing versus moving
The choice between iter and into_iter changes what flatten yields. This distinction matters for performance and ownership.
When you use iter(), you borrow the outer vector. flatten receives &Vec<T> items. It iterates over each borrowed inner vector and yields &T. The result is a stream of references. If you collect this, you get a Vec<&T>. To get owned values, you must add .copied() or .cloned(). This copies or clones each item.
When you use into_iter(), you move the outer vector. flatten receives Vec<T> items. It consumes each inner vector and yields T. The result is a stream of owned values. Collecting gives you Vec<T> directly. No cloning occurs. The inner vectors are moved into the result.
fn main() {
let nested: Vec<Vec<String>> = vec![
vec!["hello".into(), "world".into()],
vec!["foo".into()],
];
// into_iter moves the outer vec.
// flatten consumes each inner vec.
// collect builds a new vec with the moved strings.
// No cloning happens. The original nested vec is gone.
let flat: Vec<String> = nested.into_iter().flatten().collect();
println!("{:?}", flat); // ["hello", "world", "foo"]
// nested is no longer accessible here.
}
If you try to collect references into an owned vector without copying, the compiler rejects you with E0308 (mismatched types). You asked for Vec<String>, but the iterator yields &String. The compiler will not silently clone for you. You must be explicit.
Use .copied() for cheap types like integers. Use .cloned() for types that require allocation. Explicit cloning signals to readers that you considered the cost.
The flat_map upgrade
flatten is sugar for flat_map(|x| x). The flat_map adapter takes a closure that returns an iterator, then flattens the result. This is more flexible. You can transform the inner data before flattening.
Common patterns include filtering inner collections, mapping items inside, or handling errors. flat_map is the workhorse when flatten is not enough.
struct User {
name: String,
tags: Vec<String>,
}
fn main() {
let users = vec![
User { name: "Alice".into(), tags: vec!["rust".into(), "web".into()] },
User { name: "Bob".into(), tags: vec!["python".into(), "data".into()] },
];
// flat_map applies a closure to each user.
// The closure returns an iterator over the user's tags.
// flat_map then flattens all tag iterators into one stream.
// cloned() is needed because we are borrowing users.
let all_tags: Vec<String> = users
.iter()
.flat_map(|user| user.tags.iter().cloned())
.collect();
println!("{:?}", all_tags); // ["rust", "web", "python", "data"]
}
You can also filter inside the closure. If you only want tags that contain "rust", you can filter the inner iterator before flattening. This avoids pulling irrelevant data into the stream.
let rust_tags: Vec<String> = users
.iter()
.flat_map(|user| {
// Filter tags inside the closure.
// Return an iterator of matching tags.
user.tags.iter().filter(|t| t.contains("rust")).cloned()
})
.collect();
flat_map composes better than flatten when you need logic between the outer and inner levels. Reach for flat_map when you need to transform or filter before flattening.
Hidden traps: Options and Results
flatten works on any iterator of iterators. Option<T> and Result<T, E> implement IntoIterator. This means you can flatten Vec<Option<T>> or Vec<Result<Vec<T>, E>>. The behavior is subtle and can hide bugs.
Flattening Option removes the wrapper. Some(value) yields value. None yields nothing. This is useful for filtering out missing values.
let opts: Vec<Option<i32>> = vec![Some(1), None, Some(2), None];
let vals: Vec<i32> = opts.into_iter().flatten().collect();
// vals is [1, 2]. None values are silently dropped.
Flattening Result is dangerous. Result<T, E> implements IntoIterator such that Ok(value) yields value, and Err(e) yields nothing. If you flatten a Vec<Result<Vec<T>, E>>, errors are silently discarded. You lose the error information.
let results: Vec<Result<Vec<i32>, &str>> = vec![
Ok(vec![1, 2]),
Err("database timeout"),
Ok(vec![3]),
];
// flatten calls into_iter on each Result.
// Err yields nothing. The error is lost.
let flat: Vec<i32> = results.into_iter().flatten().collect();
// flat is [1, 2, 3]. The error is gone.
This is a common trap. If you have fallible data, do not use flatten unless you explicitly want to ignore errors. Use flat_map with Result::ok() if you want to filter errors, or handle errors with match or ?.
Treat flatten on Result as a code smell. If errors exist, they should be handled, not vanished.
Performance and capacity
flatten().collect() allocates a new vector. The collector grows the vector as it consumes items. This can cause multiple reallocations if the final size is large. You can hint the capacity to reduce allocations.
If you know the total number of items, reserve capacity before collecting. This is a micro-optimization. The compiler often optimizes simple chains well. Profile before optimizing.
let nested: Vec<Vec<i32>> = vec![vec![1, 2], vec![3, 4, 5]];
// Calculate total length to reserve capacity.
let total_len: usize = nested.iter().map(|v| v.len()).sum();
let mut flat = Vec::with_capacity(total_len);
flat.extend(nested.into_iter().flatten());
Using extend with a pre-allocated vector avoids the overhead of collect's growth strategy. This is useful in tight loops or when memory allocation is a bottleneck. For most code, flatten().collect() is fast enough.
Convention aside: The community prefers flatten().collect() for readability. Reserve capacity only when profiling shows allocation is the bottleneck. Don't optimize prematurely.
Pitfalls and compiler errors
If the inner items do not implement IntoIterator, flatten fails. The compiler rejects this with E0277 (trait bound not satisfied). You will see a message like the trait bound T: IntoIterator is not satisfied. This happens if you try to flatten a Vec<Vec<&T>> without care, or if you use the wrong type.
If you mix borrowing and ownership incorrectly, you get lifetime errors. flatten preserves the lifetimes of the inner items. If you collect references, the result borrows from the original data. You cannot return a flattened vector of references if the original data is local.
fn bad_flatten() -> Vec<i32> {
let nested = vec![vec![1, 2]];
// This fails. flat borrows from nested.
// nested is dropped at end of function.
// References would be dangling.
nested.iter().flatten().copied().collect()
}
This code actually compiles because .copied() creates owned values. If you remove .copied(), you get E0515 (cannot return value referencing local data). The compiler protects you from dangling references.
If you try to move out of a borrowed context, you get E0507 (cannot move out of borrowed content). This happens if you try to use into_iter on a borrowed slice. Use iter instead.
Read the error message. Rust tells you exactly what trait is missing or what lifetime is wrong. Fix the type or the borrow.
Decision matrix
Use iter().flatten().copied().collect() when you need to keep the original nested data alive and the inner items implement Copy. Use into_iter().flatten().collect() when you own the data and want to move the inner vectors into the result without cloning. Use flat_map when you need to transform or filter each inner collection before flattening, such as mapping elements or handling errors. Use a manual loop with extend when you are building the result incrementally over multiple steps and cannot express the logic as a single iterator chain. Use flatten on Option when you want to filter out missing values and discard None. Avoid flatten on Result unless you explicitly want to ignore errors; handle errors with match or ? instead.
Choose the tool that matches your ownership needs and logic complexity. The iterator chain should read like a sentence describing the data flow.