Implementing Your Own Iterators
Learn how to create custom iterators in Rust by implementing the Iterator and IntoIterator traits, with examples for consuming, borrowing, and infinite iterators
When you work with collections or custom data structures, you want them to work with for loops, .map(), .filter(), and every other iterator adapter Rust provides. The standard library's iterators cover vectors and hash maps, but they cannot walk through a type you invented. The only way to plug your own type into that entire ecosystem is to write an iterator yourself.
This section shows how to implement iterators from scratch. You will see the two fundamental patterns — making the type itself an iterator, or making a separate iterator struct that a collection produces — and you will learn to handle borrowing, lifetimes, infinite sequences, and performance hints.
How the Iterator Trait Works
At the center of everything is the Iterator trait. Its definition boils down to one required method and an associated type:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
next returns Some(item) as long as items remain, and None when iteration is finished. Once you provide that, the trait gives you dozens of adapter methods for free — map, filter, take, collect, and many more — all built on top of repeated calls to next.
The trait is designed for external iteration. The iterator struct holds the state of where you are in the sequence, and each call to next advances that state. This separation of state from the original data is what makes iterators safe to combine and chain.
Iterator Alone Does Not Give You for Loops:
Implementing Iterator on a type lets you call .next() manually and use adapter methods, but it does not make the type directly usable in a for loop. A for loop desugars to a call to .into_iter(), which requires the IntoIterator trait. We will cover that shortly.
Two Core Patterns for Custom Iterators
There are two common ways to give a type iteration capabilities. The choice depends on where the iteration state lives.
The type itself is the iterator. The struct holds all the state it needs — a current value, an index, a reference — and implements Iterator directly. This is natural for generators like counters or sequences that are not tied to a separate collection.
The type produces an iterator. The type (usually a collection) implements IntoIterator. Its into_iter method creates and returns a separate iterator struct that holds the state. This is how Vec, HashMap, and most standard collections work. It keeps the collection clean and allows multiple forms of iteration — by value, by immutable reference, and by mutable reference.
Both patterns are important. You will often find that even when the type itself could be the iterator, it is clearer to separate the state into a dedicated struct, especially when lifetimes are involved.
Pattern 1 — The Type Is the Iterator
When the iterator is a self-contained sequence, the simplest design is to put the state directly into the struct and implement Iterator on it.
A counter that yields numbers from a start to an end shows the idea clearly.
struct Counter {
current: u32,
end: u32,
}
impl Counter {
fn new(start: u32, end: u32) -> Self {
Counter { current: start, end }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end {
let value = self.current;
self.current += 1;
Some(value)
} else {
None
}
}
}
The struct stores the current position and the limit. Each call to next checks whether more values are available, saves the current value, increments the counter, and returns it. When the limit is passed, None signals the end.
Counter can now be used with every iterator method. It can be collected, filtered, summed, and chained.
let sum: u32 = Counter::new(1, 5).filter(|n| n % 2 == 0).sum();
assert_eq!(sum, 6); // 2 + 4
Free Adapter Methods:
By writing only next, you get all of Iterator's methods. The standard library provides implementations of map, filter, fold, collect, and more that call next internally. There is no extra work required to make your custom iterator composable.
A key detail: because Counter implements Iterator, it automatically implements IntoIterator as well — the blanket implementation impl<I: Iterator> IntoIterator for I returns the iterator itself. That means for value in counter works without any extra code. However, this also consumes the counter, because for takes ownership. If you need to keep the counter after the loop, you would iterate by reference, which requires a separate pattern we will cover later.
Pattern 2 — The Type Produces an Iterator
For types that represent data containers, implementing Iterator directly is usually impractical. The struct that holds the data is not the same thing as the state of iteration over it. Instead, you implement IntoIterator, which creates a separate iterator struct.
Imagine a Pixel struct that stores RGB values. You want to iterate over the three color components. The Pixel itself should not be the iterator; instead, it should produce an iterator when needed.
struct Pixel {
r: u8,
g: u8,
b: u8,
}
// Separate iterator struct that owns the Pixel data
struct PixelIntoIter {
pixel: Pixel,
index: u8,
}
impl Iterator for PixelIntoIter {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
let result = match self.index {
0 => Some(self.pixel.r),
1 => Some(self.pixel.g),
2 => Some(self.pixel.b),
_ => None,
};
self.index = self.index.wrapping_add(1);
result
}
}
impl IntoIterator for Pixel {
type Item = u8;
type IntoIter = PixelIntoIter;
fn into_iter(self) -> Self::IntoIter {
PixelIntoIter {
pixel: self,
index: 0,
}
}
}
Now a Pixel can be used directly in a for loop, which consumes it:
let p = Pixel { r: 255, g: 128, b: 64 };
for component in p {
println!("{}", component);
}
// p cannot be used here; ownership moved into the iterator
The separation of Pixel and PixelIntoIter follows the model of standard collections. The container stays simple; the iterator owns the data and the progress marker. This design becomes even more important when you add borrowing iterators next.
Borrowing Iterators and Lifetimes
Consuming the collection is not always what you want. Often you need to iterate by reference, leaving the original data intact. This requires implementing IntoIterator for &Type, yielding a borrowing iterator that holds a reference.
Consider a SensorData struct that stores a series of readings. We want a method iter() that returns an iterator over &f64 without moving the readings.
struct SensorData {
readings: Vec<f64>,
}
// Iterator that borrows the data
struct SensorIter<'a> {
data: &'a SensorData,
index: usize,
}
impl SensorData {
fn iter(&self) -> SensorIter<'_> {
SensorIter {
data: self,
index: 0,
}
}
}
impl<'a> Iterator for SensorIter<'a> {
type Item = &'a f64;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.data.readings.len() {
let item = &self.data.readings[self.index];
self.index += 1;
Some(item)
} else {
None
}
}
}
The lifetime 'a ties the iterator to the borrowed SensorData. The iterator cannot outlive the data it refers to.
To enable the for reading in &sensor_data syntax, we implement IntoIterator for a reference:
impl<'a> IntoIterator for &'a SensorData {
type Item = &'a f64;
type IntoIter = SensorIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
Now the borrowing iteration works seamlessly:
let sensor = SensorData { readings: vec![23.5, 24.1, 22.8] };
let sum: f64 = sensor.iter().sum();
let avg = sum / sensor.readings.len() as f64;
println!("Average: {:.2}", avg);
// sensor is still usable here
Lifetimes Constrain Borrowing Iterators:
If you write a borrowing iterator without proper lifetime annotations, the compiler will reject it. The borrow must be explicit: the iterator struct needs a lifetime parameter, and both the impl Iterator and impl IntoIterator blocks must connect that lifetime to the borrow of the source data. When in doubt, let the compiler suggest where to add 'a.
Mutable iteration works analogously with &mut. The iterator struct holds &'a mut SensorData and yields &'a mut f64. However, returning mutable references to individual fields from next can be tricky with the borrow checker. The standard approach is to use raw pointers in a limited unsafe block when you need to hand out disjoint mutable references, but that is an advanced pattern. For most use cases, you can avoid this complexity by iterating over indices instead of mutable references, or by restructuring the data.
Infinite Iterators
An iterator does not have to end. If next never returns None, you have an infinite sequence. The Fibonacci numbers are the classic example.
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let value = self.current;
let new_next = self.current + self.next;
self.current = self.next;
self.next = new_next;
Some(value)
}
}
This iterator never yields None. You control how many values you actually consume with adapters like take:
let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
Infinite Iterators Cause Infinite Loops If Unbounded:
If you write for _ in Fibonacci::new() { ... } or call .collect() on an infinite iterator without limiting it first, the program will never terminate. Always pair an infinite iterator with take, take_while, or another bounding adapter. The compiler does not protect you from this — it is entirely a logic contract.
Infinite iterators are valuable because they decouple sequence generation from consumption. You write the generation logic once, and each caller decides how much to take.
Optimizing with size_hint
Consumers like collect allocate memory based on a size hint the iterator provides. The default size_hint returns (0, None), meaning “I have no idea how many items are left.” That forces Vec to grow dynamically. When you know the exact remaining count, you can override size_hint to eliminate reallocations.
For the Counter that counts from current to end, we can supply an exact hint:
impl Iterator for Counter {
// ... next() as before ...
fn size_hint(&self) -> (usize, Option<usize>) {
if self.current > self.end {
return (0, Some(0));
}
let remaining = (self.end - self.current + 1) as usize;
(remaining, Some(remaining))
}
}
Now collect will pre-allocate exactly the right capacity. The hint must be truthful — the lower bound must be a real minimum, and the upper bound a real maximum. Getting this wrong will not cause memory unsafety, but it can lead to wasted allocations or under-allocations that need to reallocate later.
Exact size_hint Improves Collection Performance:
If you can provide an exact size hint where the lower and upper bounds are equal, consumers like Vec::from_iter allocate once and fill without any capacity checks. This is one of the easiest performance wins for custom iterators.
For infinite iterators, size_hint should return (usize::MAX, None) to signal unboundedness, though many infinite iterators simply leave the default.
When You Don't Need a Custom Iterator
Writing a new struct and implementing Iterator by hand gives full control, but it is not always necessary. Sometimes you can build the iterator you need entirely from existing adapters.
If you have a type that can produce an array of its fields, you can wrap an existing iterator rather than writing a new one. As of Rust 1.51, std::array::IntoIter can turn an array into an owned iterator.
impl IntoIterator for Pixel {
type Item = u8;
type IntoIter = std::array::IntoIter<u8, 3>;
fn into_iter(self) -> Self::IntoIter {
std::array::IntoIter::new([self.r, self.g, self.b])
}
}
This avoids a separate iterator struct entirely. The same idea works for borrowing: you can chain iterator combinators to produce impl Iterator<Item = &u8> from a method, using std::iter::once and .chain().
Before writing a custom iterator, ask whether your iteration logic is just a combination of existing maps, filters, and zips. If it is, composing them is often clearer and less error-prone.
Common Mistakes and How to Avoid Them
Forgetting to implement IntoIterator for references. A type that only implements Iterator directly can be consumed by for, but if the type holds data you want to reuse, you need impl IntoIterator for &Type. Many beginners assume that Iterator on the type is enough for all cases.
Off-by-one errors in next. When you increment a counter inside next, the order of returning and incrementing matters. The pattern of saving the current value, then advancing, then returning the saved value is the safest. Reversing it causes you to skip the first element or to go one past the end.
Incorrect size_hint. Providing a lower bound that is higher than the actual item count can cause collect to allocate too much, but it does not cause undefined behavior. More dangerously, an infinite iterator that reports a finite size hint can lead consumers into infinite loops because they think the end is near. Always ensure the hint reflects reality.
Holding mutable references across next calls incorrectly. If your iterator borrows data mutably and tries to return a mutable reference to a part of it, the borrow checker may prevent you from implementing next at all. This is a signal to reconsider the design — perhaps iterate over indices instead, or separate the data into disjoint pieces that can be borrowed independently.
Unsafe Blocks in Mutable Iterators:
Some examples online show mutable iterators using raw pointers and unsafe to bypass the borrow checker. This can work if done correctly — the fields being returned are disjoint — but it introduces the risk of aliasing violations and undefined behavior. Prefer safe patterns unless you have measured that raw pointer manipulation is necessary and you can prove its correctness.
Summary
Custom iterators bridge your types and Rust's entire functional ecosystem. The two patterns — making the type itself an iterator or creating a separate iterator struct through IntoIterator — cover almost every real-world need. Borrowing iterators add lifetimes to let you iterate without consuming. Infinite iterators remove the endpoint, and size_hint tunes performance.
The work required is modest: define the state, write next, and if appropriate, implement IntoIterator for owned and borrowed variants. The reward is that your type instantly gains map, filter, fold, collect, and every other adapter — exactly like a standard collection.
When you next design a data structure that stores multiple values, implement iteration for it before anything else. Iteration is the universal interface that allows your code to compose with the rest of the language, and once you have it, the rest of the design often becomes clearer.