The Iterator Trait

An iterator is a value that knows how to produce a sequence of items, one at a time, on demand. In Rust, this idea is not built into the language as special syntax — it is a single trait, Iterator, with one required method. Everything else you associate with iteration in Rust — for loops, map, filter, collect, and dozens of other tools — is built on top of that one method. Understanding this trait is the key to writing idiomatic, fast Rust code instead of hand-rolled index loops.

Overview: How Iterators Work

At its core, the Iterator trait looks like this: it has an associated type Item (the kind of value it yields) and one required method, next, which returns Option<Self::Item>. Calling next() either returns Some(value) — here is the next item, and the iterator’s internal state has advanced — or None, meaning the sequence is exhausted. That is the entire contract. There is no separate "has more items" check; the Option return value does both jobs at once.

Once a type implements just that one method, it automatically gets dozens of other methods for free — map, filter, zip, take, fold, sum, collect, and more — because the Iterator trait provides them as default methods implemented purely in terms of next(). This is why implementing Iterator for your own type is so cheap: you write five or ten lines for next(), and the standard library hands you an entire toolkit.

The second crucial idea is laziness. Calling v.iter().map(|x| x * 2) does not loop over anything yet — it just builds a new iterator value that remembers the mapping function and wraps the original iterator. No work happens until something actually pulls values out by calling next(), either directly, through a for loop, or through a consuming method like collect() or sum(). Methods that return another iterator (like map and filter) are called adapters; methods that drive the iteration to completion and produce a final value are called consumers. A chain of adapters followed by one consumer compiles down to a single tight loop with no intermediate allocations — this is Rust’s "zero-cost abstraction" promise in action.

Finally, there is a related trait, IntoIterator, which is what actually powers the for loop. A for x in collection loop desugars to calling collection.into_iter() and then repeatedly calling .next() on the result until it returns None. Most collections offer three flavors of this: .iter() yields shared references (&T), .iter_mut() yields mutable references (&mut T), and .into_iter() yields owned values (T), consuming the collection in the process. Choosing the right one matters — using into_iter() on a Vec moves it, so you cannot use that vector afterward.

Syntax

The trait itself is defined (in simplified form) like this:

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // plus dozens of default methods built on top of next(),
    // such as map, filter, zip, take, fold, sum, and collect
}
  • type Item — an associated type declaring what kind of value this iterator produces.
  • fn next(&mut self) -> Option<Self::Item> — the single required method; it takes a mutable reference because advancing the iterator changes its internal state.
  • Every other method (map, filter, collect, …) has a default implementation written in terms of next(), so implementers only need to supply next().

Some of the most common methods you will reach for:

Method Category What it does
next() required Returns the next item as Some(item), or None when exhausted
map(f) adapter (lazy) Transforms each item using a closure
filter(pred) adapter (lazy) Keeps only items for which pred returns true
zip(other) adapter (lazy) Pairs items from two iterators, stopping at the shorter one
enumerate() adapter (lazy) Pairs each item with its index, starting at 0
take(n) / skip(n) adapter (lazy) Limits to the first n items, or skips the first n
collect() consumer Builds a collection (Vec, String, HashMap, …) from the items
sum() / count() / fold() consumer Reduces the whole sequence to a single value
for_each(f) consumer Runs a closure on every item, discarding the results

Examples

Example 1: Calling next() by hand

Before reaching for adapters, it helps to see the raw mechanism. Here we get an iterator with .iter() and pull items out manually, then let a for loop do the same work automatically.

fn main() {
    let numbers = vec![10, 20, 30];
    let mut iter = numbers.iter();

    println!("{:?}", iter.next());
    println!("{:?}", iter.next());
    println!("{:?}", iter.next());
    println!("{:?}", iter.next());

    for n in &numbers {
        println!("value: {}", n);
    }
}

Output:

Some(10)
Some(20)
Some(30)
None
value: 10
value: 20
value: 30

Each call to next() returns Some(&i32) until the vector is exhausted, at which point it returns None forever. The for loop below does exactly the same thing internally — it just hides the repeated next() calls.

Example 2: A custom iterator

Implementing Iterator for your own type only requires writing next(); every adapter and consumer method becomes available automatically.

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {
    let sum: u32 = Counter::new()
        .zip(Counter::new().skip(1))
        .map(|(a, b)| a * b)
        .filter(|x| x % 3 == 0)
        .sum();

    println!("sum = {}", sum);

    let collected: Vec<u32> = Counter::new().collect();
    println!("{:?}", collected);
}

Output:

sum = 18
[1, 2, 3, 4, 5]

Counter::new() yields 1, 2, 3, 4, 5. The second counter, after .skip(1), yields 2, 3, 4, 5. zip pairs them into (1,2), (2,3), (3,4), (4,5); map multiplies each pair to get 2, 6, 12, 20; filter keeps only multiples of 3, leaving 6 and 12; and sum adds them to 18. None of this ran until .sum() was called — the chain of adapters was just a lazy plan until then. Notice we never wrote map, zip, filter, sum, or collect ourselves; Counter got all of them by implementing only next().

Example 3: Filtering and mapping real data

A more realistic case: pulling long words out of a list and uppercasing them, plus computing a total.

fn main() {
    let words = vec![
        String::from("rust"),
        String::from("is"),
        String::from("blazingly"),
        String::from("fast"),
    ];

    let long_words: Vec<String> = words
        .iter()
        .filter(|w| w.len() > 3)
        .map(|w| w.to_uppercase())
        .collect();

    println!("{:?}", long_words);

    let total_chars: usize = words.iter().map(|w| w.len()).sum();
    println!("total characters: {}", total_chars);
}

Output:

["RUST", "BLAZINGLY", "FAST"]
total characters: 19

Using .iter() instead of .into_iter() is important here: it borrows each String rather than moving it, so words is still fully usable on the next line for the total_chars calculation. If we had used words.into_iter() in the first pipeline, words would have been moved and the second line would fail to compile.

How It Works Step by Step

When the compiler sees for x in v { ... }, it does not treat this as magic syntax. It desugars roughly to the following, using IntoIterator::into_iter and a while let loop around next():

let v = vec![1, 2, 3];
let mut iter = v.into_iter();
while let Some(x) = iter.next() {
    println!("{}", x);
}

Output:

1
2
3

Step by step: v.into_iter() consumes the vector and produces an iterator that owns its elements. Each loop iteration calls iter.next(): while it returns Some(x), the loop body runs with x bound to that value; the moment it returns None, the loop ends. This is exactly what a for loop does under the hood — there is no separate looping construct in the compiler for collections, just this one pattern applied to anything implementing IntoIterator. Adapter chains work the same way: each adapter’s next() pulls from the iterator underneath it, transforms or filters the result, and passes it up, all driven by a single outer call to next() from the final consumer. Because this all monomorphizes at compile time (no dynamic dispatch involved for concrete adapter chains), the resulting machine code is typically as fast as a hand-written loop.

Common Mistakes

Mistake 1: Using a collection after a consuming for loop

Writing for x in v (rather than for x in &v) calls v.into_iter(), which moves v. Trying to use v afterward is a compile error, not a runtime bug — the borrow checker catches it before the program ever runs.

fn main() {
    let v = vec![1, 2, 3];

    for x in v {
        println!("{}", x);
    }

    println!("{:?}", v); // error: value borrowed here after move
}

The fix is to iterate by reference with &v (or v.iter()), which borrows each element instead of taking ownership, leaving v valid afterward:

fn main() {
    let v = vec![1, 2, 3];

    for x in &v {
        println!("{}", x);
    }

    println!("{:?}", v);
}

Mistake 2: Forgetting mut on an iterator binding

Calling .next() requires &mut self, because advancing an iterator changes its internal position. If the iterator variable itself is not declared mut, this fails to compile.

fn main() {
    let v = vec![1, 2, 3];
    let iter = v.iter();
    println!("{:?}", iter.next()); // error: cannot borrow `iter` as mutable
}

Adding mut to the binding fixes it:

fn main() {
    let v = vec![1, 2, 3];
    let mut iter = v.iter();
    println!("{:?}", iter.next());
}

Note that this only comes up when you call .next() directly; a for loop never requires you to write mut yourself, because the desugared code introduces its own hidden mut binding for you.

Best Practices

  • Prefer .iter() for read-only access, .iter_mut() when you need to modify elements in place, and .into_iter() only when you genuinely want to consume and own the values.
  • Chain adapters (map, filter, zip, …) instead of writing manual index-based loops — they are just as fast and far less error-prone (no off-by-one bugs).
  • Remember that adapters are lazy: a chain that ends without a consumer like collect(), sum(), for_each(), or a for loop does nothing at all.
  • Use enumerate() instead of maintaining a manual counter variable when you need both the index and the value.
  • When implementing your own type’s Iterator, keep next() simple and correct; you get the entire adapter/consumer toolkit for free.
  • Reach for collect() only when you actually need the resulting collection stored; if you just need to process items once, iterate directly without collecting first.

Practice Exercises

  • Given let nums = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];, use iterator adapters to compute the sum of the squares of only the even numbers. Expected result: 220.
  • Implement your own Iterator for a struct called Fibonacci that yields the Fibonacci sequence (0, 1, 1, 2, 3, 5, …). Use .take(8).collect::<Vec<u64>>() to print the first eight values.
  • Given let names = vec!["Ann", "Bo", "Charlie", "Dee"];, use enumerate() and a for loop to print each name alongside its 1-based position, e.g. 1: Ann.

Summary

  • The Iterator trait requires only one method, next(&mut self) -> Option<Self::Item>, and provides dozens of adapter and consumer methods for free on top of it.
  • for loops are not special syntax for collections — they desugar to IntoIterator::into_iter() followed by repeated calls to next().
  • Adapters like map, filter, and zip are lazy and build a pipeline; consumers like collect, sum, and for_each actually drive iteration.
  • Use .iter() for borrowed access, .iter_mut() for mutable borrowed access, and .into_iter() for owned, moving access — picking the wrong one is a common source of move errors.
  • Calling .next() directly requires a mut binding, since advancing an iterator mutates its internal state.
  • Implementing Iterator for your own types is cheap and gives you the full standard-library iterator toolkit automatically.