When a Vec isn't the answer
You're building a log parser that reads lines from a file, but you want to filter out comments and trim whitespace before the rest of your code touches the data. Or you're writing a game where enemies spawn based on a complex schedule, and you need a stream of spawn events. You can't just put everything in a Vec because the data is infinite, or too large, or generated dynamically. You need a way to produce values one by one, on demand. That's where implementing the Iterator trait yourself comes in.
The iterator contract
The Iterator trait is a contract. It says your type can produce a sequence of values. The core of the contract is one method: next. Every time someone calls next, your struct must return the next value in the sequence, or signal that the sequence is finished.
The return type is always Option<Self::Item>. Some(value) means "here is the next item." None means "I'm done. Stop asking." The Item associated type tells the compiler what kind of values you produce. If you're yielding integers, Item is i32. If you're yielding strings, Item is String.
Think of an iterator as a ticket booth at a theme park. The booth doesn't hold all the tickets in its hand. It has a machine that prints them. You ask for a ticket, it prints one and hands it to you. You ask again, it prints another. Eventually, the machine runs out of ink or paper, and it tells you "no more." In Rust, that "no more" is None. The ticket is Some(value).
The contract is simple: produce a value or say stop.
Minimal example
Here is a counter that yields numbers from 1 to 5. It implements Iterator so it works with standard adapters like take and sum.
/// A counter that yields numbers starting from 1 up to 5.
struct Counter {
current: u32,
}
impl Iterator for Counter {
// The Item associated type declares what this iterator yields.
type Item = u32;
// next is the only required method. It drives the iteration.
fn next(&mut self) -> Option<Self::Item> {
// Increment the internal state before returning.
self.current += 1;
// Return the value if we haven't exceeded the limit.
if self.current <= 5 {
Some(self.current)
} else {
// Return None to signal the iterator is exhausted.
None
}
}
}
fn main() {
// Create the iterator. It hasn't produced anything yet.
let counter = Counter { current: 0 };
// Adapters like take and sum work because Counter implements Iterator.
let sum: u32 = counter.take(3).sum();
println!("Sum: {sum}"); // Prints "Sum: 6" (1 + 2 + 3)
}
Iterators in Rust are lazy. Calling counter.take(3) doesn't run the loop. It builds a pipeline. The work happens when you call sum or loop over it. This is a community expectation. If your next method has side effects, they only happen when the iterator is consumed.
Adapters chain together because they all speak the Iterator language.
How the chain executes
When you call counter.take(3), Rust creates a Take adapter struct that wraps your Counter. The Take struct holds a reference to the counter and a limit. It also implements Iterator. When sum starts pulling values, it calls next on the Take adapter. The adapter checks its limit, calls next on the underlying Counter, and forwards the result. This chain continues until None bubbles up.
The compiler generates this chain as efficient as a hand-written loop. There's no hidden allocation. No virtual dispatch overhead unless you use trait objects. The next calls are inlined. Rust iterators are zero-cost abstractions. When you chain filter, map, and collect, the compiler generates code equivalent to a hand-written loop with if checks and variable assignments. There is no intermediate vector created between stages. The filter passes values directly to the map. The map passes them to the collect.
Trust the compiler to inline the chain. You get loop performance with pipeline syntax.
Realistic example: wrapping I/O
A common use case is wrapping an external resource like a file or network stream. Here is a ChunkReader that reads a file in fixed-size chunks. This avoids loading the entire file into memory.
use std::fs::File;
use std::io::Read;
/// Reads a file in fixed-size chunks to avoid loading the whole file.
struct ChunkReader {
file: File,
buffer: Vec<u8>,
chunk_size: usize,
}
impl ChunkReader {
/// Creates a new ChunkReader for the given file path.
fn new(path: &str, chunk_size: usize) -> std::io::Result<Self> {
let file = File::open(path)?;
// Pre-allocate the buffer to avoid resizing on every read.
let buffer = vec![0; chunk_size];
Ok(Self { file, buffer, chunk_size })
}
}
impl Iterator for ChunkReader {
// Yielding a Result allows the iterator to propagate errors.
type Item = std::io::Result<Vec<u8>>;
fn next(&mut self) -> Option<Self::Item> {
// Reuse the buffer. Clear it to prepare for the next read.
self.buffer.clear();
// Attempt to read up to chunk_size bytes.
match self.file.read(&mut self.buffer) {
Ok(0) => {
// 0 bytes read means EOF. The iterator is done.
None
}
Ok(n) => {
// Truncate the buffer to the actual bytes read.
self.buffer.truncate(n);
Some(Ok(self.buffer.clone()))
}
Err(e) => {
// Propagate the error. The caller can decide to stop or handle it.
Some(Err(e))
}
}
}
}
fn main() -> std::io::Result<()> {
let reader = ChunkReader::new("large_file.bin", 1024)?;
// Process chunks one by one. The file is never fully in memory.
for chunk_result in reader {
let chunk = chunk_result?;
// Process chunk...
println!("Read {} bytes", chunk.len());
}
Ok(())
}
When wrapping external resources, keep the iterator struct small. Store the connection handle or client in the struct, not in the next method. The iterator owns the state. If Item is a Result, adapters like filter_map or flatten become useful. You can also collect::<Result<Vec<_>, _>>() to gather all items and stop at the first error.
Wrap the messy I/O in a clean iterator. The rest of your code stays pure.
Pitfalls and compiler errors
If you forget to define type Item, the compiler rejects the implementation with E0192 (associated type Item is not implemented). You must declare what you yield.
If your next method returns a reference to data that gets dropped, you'll hit lifetime errors. The iterator owns its state. If you store a &str inside the iterator, the iterator's lifetime is tied to that string. If you try to return Some(&self.data) where data is owned by the iterator, you get E0515 (cannot return value referencing local variable). The iterator must own the data it yields, or the data must live longer than the iterator.
Another trap is infinite loops. If your next method never returns None, the iterator runs forever. Adapters like take can prevent this, but relying on them is fragile. Your iterator should be self-terminating.
Some adapters assume the iterator is "fused," meaning once it returns None, it keeps returning None. If your iterator can recover from None or produce values after exhaustion, you might break these adapters. The community convention is to implement FusedIterator if your iterator guarantees this behavior, or to document the non-standard behavior.
Return owned data or long-lived references. Don't leak internal pointers.
Making your type iterable with for loops
The for loop doesn't call next directly. It calls into_iter. If you want for item in my_struct to work, you must implement IntoIterator. The IntoIterator trait has an into_iter method that consumes self and returns an iterator.
Usually, you implement IntoIterator by returning a custom iterator struct. This separates the collection from the iteration state. The collection can be cloned, but the iterator holds the position.
struct MyCollection {
data: Vec<i32>,
}
struct MyIter {
data: Vec<i32>,
index: usize,
}
impl Iterator for MyIter {
type Item = i32;
fn next(&mut self) -> Option<i32> {
if self.index < self.data.len() {
let value = self.data[self.index];
self.index += 1;
Some(value)
} else {
None
}
}
}
impl IntoIterator for MyCollection {
type Item = i32;
type IntoIter = MyIter;
fn into_iter(self) -> Self::IntoIter {
// Consume the collection and return the iterator.
MyIter { data: self.data, index: 0 }
}
}
fn main() {
let collection = MyCollection { data: vec![10, 20, 30] };
// This works because MyCollection implements IntoIterator.
for value in collection {
println!("{value}");
}
}
If your struct is the iterator itself, you don't need IntoIterator. The compiler handles it. Lines from std::io implements Iterator directly. You can write for line in file.lines(). The struct holds the file handle and the buffer, and next reads the next line.
Convention aside: Rc::clone(&data) vs data.clone(). Both compile, both work. Convention is the explicit form because data.clone() looks like a deep clone but isn't. Apply the same clarity to iterator construction. Name your iterator struct explicitly. Don't hide the type behind a trait object unless you need dynamic dispatch.
When to use what
Use a custom Iterator when you need to generate values on demand and avoid allocating a full collection in memory.
Use a custom Iterator when you're wrapping an external resource like a file, network stream, or database cursor and want to expose a uniform interface.
Use IntoIterator when your type can be consumed to produce an iterator, allowing for item in my_type syntax. Implement IntoIterator for the collection, and return your custom iterator.
Use std::iter::successors when your sequence is defined by a starting value and a transformation function, like a linked list traversal or a countdown.
Use std::iter::from_fn when you have a closure that produces the next value and you don't need to maintain complex internal state.
Pick the tool that matches your data source. Don't force a Vec where a stream belongs.