Iterators in Rust From For Loops to Adapter Chains
Iterators are lazy: adapters like map and filter do nothing until a consumer such as collect or sum runs. See the chain, the traits and the common traps.

A for loop tells the computer how to walk a collection. An iterator chain tells it what you want from the collection and leaves the walking to the compiler. Rust's iterators are the reason code like words.iter().filter(...).map(...).collect() is both idiomatic and fast. This post runs the main pieces, proves the laziness with a program that prints, shows the two errors beginners meet most, and implements an iterator by hand. All programs ran on Rust 1.99, edition 2024.
A first chain
fn main() {
let words = ["apple", "fig", "banana", "kiwi", "cherry", "date"];
let result: Vec<String> = words
.iter() // yields &&str
.filter(|w| w.len() > 3) // keep long words
.map(|w| w.to_uppercase()) // transform each
.collect(); // consume into a Vec
println!("{result:?}");
let total_len: usize = words.iter().map(|w| w.len()).sum();
println!("total length {total_len}");
let longest = words.iter().max_by_key(|w| w.len());
println!("{longest:?}");
let (short, long): (Vec<&str>, Vec<&str>) = words.iter().partition(|w| w.len() <= 4);
println!("{short:?} | {long:?}");
}["APPLE", "BANANA", "KIWI", "CHERRY", "DATE"]
total length 28
Some("cherry")
["fig", "kiwi", "date"] | ["apple", "banana", "cherry"]The first expression is the shape to learn. words.iter() produces an iterator over references. filter keeps the words longer than three letters, map uppercases them and collect gathers the result into a Vec<String>. Each step is an adapter that takes an iterator and returns another one; the last is a consumer that actually pulls the items through. The other lines show common consumers: sum totals the lengths (28), max_by_key picks the longest word, and partition splits one pass into two vectors.
Look at the second line of output for a detail that catches people. banana and cherry both have six letters, and max_by_key returned cherry: when several elements tie for the maximum it returns the last one, while min_by_key returns the first. If ties matter, say how you want them broken.
Nothing happens until you ask
Adapters are lazy. Building a chain does no work; the work happens when a consumer pulls on the end. You can watch it:
fn main() {
let numbers = [1, 2, 3, 4, 5, 6];
let doubled = numbers.iter().map(|n| {
println!(" mapping {n}");
n * 2
});
println!("built the iterator, nothing printed yet");
let first_big = doubled.filter(|n| *n > 6).next();
println!("first > 6: {first_big:?}");
}built the iterator, nothing printed yet
mapping 1
mapping 2
mapping 3
mapping 4
first > 6: Some(8)The map closure prints a line whenever it runs. The first message, "built the iterator", appears before any mapping, so creating doubled did nothing. Then filter(...).next() asks for one item that is greater than 6. The pipeline pulls 1, 2, 3 and 4 through the map, and stops at 4 doubled to 8, the first value above 6. The elements 5 and 6 were never mapped. A hand-written loop that built a full doubled vector first would have done all six.
This is why chains with find, take, any and position can be cheap on large inputs, and why iterators and loops perform the same: there is no intermediate collection between the steps, just one loop that the compiler fuses.
Three ways to iterate a vector
Whether a loop borrows, mutates or consumes depends on which method you call:
fn main() {
let mut names = vec![String::from("ada"), String::from("grace")];
for n in names.iter() { // &String: borrow
println!("{n}");
}
for n in names.iter_mut() { // &mut String: modify in place
n.push('!');
}
for n in &names { // same as .iter()
print!("{n} ");
}
println!();
for n in names { // String: consumes the vector
print!("{} ", n.len());
}
println!();
}ada
grace
ada! grace!
4 6 names.iter() yields &String, so the loop can read but not change. names.iter_mut() yields &mut String, so n.push('!') changed the strings in place, which the next loop shows with ada! and grace!. for n in &names is shorthand for .iter(). The last loop, for n in names, calls into_iter() and takes ownership of the vector: it yields owned Strings, and names cannot be used afterwards. That is the choice behind many "value moved" errors, covered in ownership.
A related mistake is calling an adapter on the collection itself:
fn main() {
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.map(|n| n * 2).collect();
println!("{doubled:?}");
}error[E0599]: no method named `map` found for struct `Vec<{integer}>` in the current scope
--> src/main.rs:3:31
|
3 | let doubled: Vec<i32> = v.map(|n| n * 2).collect();
| ^^^ `Vec<{integer}>` is not an iterator
|
help: call `.into_iter()` first
|
3 | let doubled: Vec<i32> = v.into_iter().map(|n| n * 2).collect();A Vec is not an iterator, it is something you can make one from. The help text tells you to call .into_iter() first, or .iter() if you want to keep the vector.
collect needs to know what to build
collect can build many containers, so it cannot guess:
fn main() {
let words = ["a", "b"];
let upper = words.iter().map(|w| w.to_uppercase()).collect();
println!("{}", upper.len());
}error[E0282]: type annotations needed
--> src/main.rs:3:9
|
3 | let upper = words.iter().map(|w| w.to_uppercase()).collect();
| ^^^^^
4 | println!("{}", upper.len());
| ----- type must be known at this point
|
help: consider giving `upper` an explicit type
|
3 | let upper: Vec<_> = words.iter().map(|w| w.to_uppercase()).collect();
| ++++++++The compiler cannot tell whether upper should be a Vec, a HashSet or a String because upper.len() works on all of them. Give it the type, and there are three ways:
use std::collections::{BTreeMap, HashSet};
fn main() {
let words = ["b", "a", "b", "c"];
let v = words.iter().collect::<Vec<_>>(); // turbofish on collect
let set: HashSet<_> = words.iter().collect(); // annotation on the binding
let counts = words.iter().fold(BTreeMap::new(), |mut m, w| {
*m.entry(*w).or_insert(0) += 1;
m
});
let joined = words.iter().copied().collect::<Vec<_>>().join("+");
let parsed: Result<Vec<i32>, _> = ["1", "2", "x"].iter().map(|s| s.parse::<i32>()).collect();
println!("{v:?} {} unique", set.len());
println!("{counts:?}");
println!("{joined}");
println!("{parsed:?}");
}["b", "a", "b", "c"] 3 unique
{"a": 1, "b": 2, "c": 1}
b+a+b+c
Err(ParseIntError { kind: InvalidDigit })collect::<Vec<_>>() is the turbofish: the _ lets the compiler infer the element type while you name the container. let set: HashSet<_> = ... puts the annotation on the binding instead. fold builds a BTreeMap of counts by threading an accumulator through each element, and the Result<Vec<i32>, _> line shows the most useful collect trick: it gathers values, but stops and returns the first Err, here InvalidDigit for "x". That turns "parse every field or fail" into one expression instead of a loop with an early return. The same applies to Option.
Short-circuiting and infinite iterators
Some consumers stop early, so the iterator may never end:
fn main() {
let big = (1..).map(|n| n * n); // infinite
let first: Vec<u64> = big.clone().take(5).collect();
println!("{first:?}");
let found = big.clone().find(|n| n % 7 == 0 && *n > 100);
println!("{found:?}");
println!("{}", big.clone().take_while(|n| *n < 50).count());
println!("{}", [3, 5, 8].iter().any(|n| n % 2 == 0));
println!("{}", [3, 5, 8].iter().all(|n| n % 2 == 1));
println!("{:?}", [3, 5, 8].iter().position(|&n| n == 5));
println!("{:?}", (1..=4).rev().collect::<Vec<_>>());
}[1, 4, 9, 16, 25]
Some(196)
7
true
false
Some(1)
[4, 3, 2, 1](1..) counts forever, and .map(|n| n * n) squares every number. take(5) keeps the first five squares. find stops at the first square that is divisible by 7 and above 100, which is 196. take_while(|n| *n < 50).count() counts 7 squares below 50 (1, 4, 9, 16, 25, 36, 49). The booleans from any and all, the index from position and the reversed range all stop as soon as the answer is known. The clone() calls are there because consuming an iterator uses it up; a cloneable adapter chain can be restarted.
Writing your own
The trait has one required method:
struct Countdown(u32);
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.0 == 0 {
None
} else {
self.0 -= 1;
Some(self.0 + 1)
}
}
}
fn main() {
let v: Vec<u32> = Countdown(5).collect();
println!("{v:?}");
// All adapters work on it for free.
let evens: Vec<u32> = Countdown(10).filter(|n| n % 2 == 0).map(|n| n * n).collect();
println!("{evens:?}");
println!("{}", Countdown(4).zip(Countdown(100)).map(|(a, b)| a * b).sum::<u32>());
}[5, 4, 3, 2, 1]
[100, 64, 36, 16, 4]
990Countdown implements next, returning Some until it hits zero. That is all it needs: collect, filter, map, zip and sum come with the trait. The last line zips the countdown from 4 with the countdown from 100, multiplies the pairs and sums them: 4 x 100 + 3 x 99 + 2 x 98 + 1 x 97 = 990. Zipping stopped when the shorter side ended. Implementing Iterator is the right way to expose a sequence from your own type, and IntoIterator is the trait that makes for x in my_type work.
Habits
- Use a loop when you need
breakwith complicated state or several mutable variables, and a chain when each step is a transformation. - Write the type of a
collecttarget once, in the place that reads best. - Avoid collecting into a
Vecin the middle of a chain just to iterate it again; pass the iterator along. - Use
sum::<i32>()orsum::<u64>()when the compiler cannot infer the type. - Reach for parallel iterators only after the sequential version works, and measure.
When a loop is clearer
A long chain with nested closures and four adapters can be harder to debug than a loop: there is no line to put a println! on. If a chain stops being readable, split it into named intermediate iterators (let long_words = words.iter().filter(...);) or a helper function. And for code where you need the index and the value and the previous value together, a plain for i in 0..n or windows(2) often says it better than zip and skip.


