How to Implement Iterator for Custom Types

Implement the Iterator trait by defining the Item type and the next method to return Option values.

When loops feel clunky

You've built a custom structure that holds a sequence of values. Maybe a parser that yields tokens one by one, or a game engine that tracks active entities, or a range generator that computes numbers on the fly. You want to write for item in my_struct { ... } or chain .filter().map().collect(). Right now, you're stuck writing manual index loops, exposing internal arrays, or duplicating logic every time you need to traverse the data.

Rust has a better way. Implementing the Iterator trait unlocks the entire standard library ecosystem for your type. You get for loops, adapters like map and filter, and methods like collect and sum for free. The trait is small, but the payoff is massive.

The ticket dispenser model

Think of an iterator as a ticket dispenser at a deli. The machine holds the state: which number is next. You press the button, it gives you a ticket. You press it again, you get the next one. Eventually, the machine runs out of tickets and tells you "empty."

The Iterator trait is just the interface for that button press. It asks two questions:

  1. What kind of thing do you produce?
  2. Give me the next one, or tell me you're done.

The trait defines an associated type Item for the first question. It defines a method next for the second. The next method returns Option<Self::Item>. This handles the "done" case without panics, special return codes, or exceptions. When next returns None, the iterator is exhausted.

Minimal implementation

Here is the skeleton. You define a struct to hold state, then implement Iterator.

/// A simple counter that yields numbers from 1 to 5.
struct Counter {
    current: u32,
}

impl Iterator for Counter {
    // Define the type this iterator produces.
    type Item = u32;

    /// Returns the next value, or None when exhausted.
    fn next(&mut self) -> Option<Self::Item> {
        // Check if we have values left.
        if self.current < 5 {
            // Advance state before returning.
            self.current += 1;
            Some(self.current)
        } else {
            // Signal exhaustion.
            None
        }
    }
}

The next method takes &mut self. This is the key insight. An iterator must change its internal state to produce the next value. If you call next twice, the second call must see the updated state. The signature requires a mutable reference because the iterator mutates itself.

Community convention: use Self::Item in the return type of next rather than repeating the concrete type. It keeps the implementation tied to the associated type definition. If you change Item, the return type updates automatically.

How for loops use your iterator

The for loop is syntactic sugar. When you write for x in value, the compiler looks for IntoIterator. If value implements Iterator, it also implements IntoIterator by default. The loop calls into_iter to get an iterator, then calls next in a loop until None appears.

This means your Counter works in a loop immediately.

fn main() {
    let mut counter = Counter { current: 0 };

    // The loop calls next() repeatedly.
    for num in &mut counter {
        println!("Got: {}", num);
    }
}

Note the &mut counter. The loop needs a mutable reference because next takes &mut self. If you try to iterate over an immutable reference, the compiler rejects you with E0277 (trait bound not satisfied). The iterator must be able to mutate its state.

Realistic example: Fibonacci with a limit

A counter is simple. Real iterators often track multiple pieces of state. Here is a Fibonacci generator that yields numbers up to a limit.

/// Yields Fibonacci numbers up to a maximum value.
struct Fib {
    current: u64,
    next: u64,
    limit: u64,
}

impl Iterator for Fib {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        // Stop if the current value exceeds the limit.
        if self.current > self.limit {
            return None;
        }

        // Capture the value to return.
        let result = self.current;

        // Compute the next pair for the following call.
        let new_next = self.current + self.next;
        self.current = self.next;
        self.next = new_next;

        Some(result)
    }
}

Now you can chain adapters. The adapters don't run immediately. They build a pipeline. The computation happens when you call a consumer like collect or sum.

fn main() {
    let fib = Fib {
        current: 0,
        next: 1,
        limit: 100,
    };

    // This creates a pipeline. Nothing runs yet.
    let filtered = fib.filter(|&x| x % 2 == 0);

    // This consumes the pipeline and runs the logic.
    let even_fibs: Vec<u64> = filtered.collect();
    println!("{:?}", even_fibs);
}

The lazy evaluation is a feature. You can build complex chains without intermediate allocations. The Iterator trait makes this possible because every adapter returns a new iterator that wraps the previous one.

The exhaustion contract

There is a strict contract for Iterator. Once next returns None, all subsequent calls must return None. The iterator is dead.

Breaking this contract causes undefined behavior in some adapters or infinite loops in others. Adapters like collect rely on None to stop. If next returns None and then later returns Some, collect will keep asking forever.

Respect the None contract. Once it's gone, it's gone.

Common pitfalls and errors

If you define fn next(&self), the compiler rejects you with E0195 (method has incompatible signature). The trait requires &mut self. You cannot implement Iterator on a type that cannot mutate its state.

If you forget type Item, you get E0191 (associated type is missing). The compiler needs to know the output type to type-check the rest of the code.

If your next method panics, the iterator is broken. Adapters will propagate the panic. This is usually fine, but be aware that next should be cheap and stable. Heavy computation or I/O in next kills performance. Adapters call next in tight loops.

Community convention: keep next fast. Document the exhaustion condition. Readers need to know when None happens. If your iterator depends on external state, mention it in the doc comment.

IntoIterator: the bridge to loops

You often want a type to work in a for loop, but the type itself isn't the iterator. Vec is the classic example. Vec stores data. It doesn't track a cursor. Vec implements IntoIterator, which converts the vector into an iterator.

You can implement IntoIterator for your custom type. This allows for x in my_type syntax.

impl IntoIterator for Counter {
    type Item = u32;
    type IntoIter = Counter;

    fn into_iter(self) -> Self::IntoIter {
        self
    }
}

Now you can write for i in Counter { ... }. The into_iter method takes ownership and returns the iterator. If your type is already the iterator, into_iter just returns self.

Use IntoIterator when you want a collection type to feel native in loops. It separates the data container from the traversal logic.

Decision matrix

Use Iterator when your type generates values on demand and needs to track progress between calls. Use IntoIterator when you want a collection type to work directly in a for loop by converting itself into an iterator. Use std::iter::successors when you can define the next value as a pure function of the current value without extra state. Use a plain Vec when the data is small, fits in memory, and you don't care about lazy evaluation.

Lazy evaluation wins when the sequence is large or infinite. Implement Iterator to get the performance and composability of the standard library.

Where to go next