codejump

AcademyDesign Patterns

Iterator pattern

Walking a collection without exposing how it is stored, the extra object between caller and data it costs, and the wrong answer when next() runs past the end or data mutates.

5 min readUpdated

3 exercises

Waiting at the end of this page.

Iterator is for walking a collection without the caller ever needing to know how that collection is actually stored underneath.

An Iterator presents a uniform way to step through a collection, hasNext() and next(), without the caller ever touching the underlying storage. Swap an array for a linked list or a database cursor behind it, and every loop written against the iterator keeps working unchanged.

The problem it solves

You have a list of radio stations stored in an array. Code elsewhere walks it with an index:

TypeScript
for (let i = 0; i < stations.length; i++) use(stations[i]);

Then the data moves: a tree of categories, a linked list, a result set that arrives page by page from an API. Every loop that assumed an array breaks.

  • Callers depend on the storage. They know it is indexed and has a length.
  • Traversal logic is repeated. Every caller writes its own walk, and each one can get it wrong.
  • Some collections have no index. A tree or a stream has no stations[i].

The fix is to give the collection an object that knows how to walk it and exposes the same small interface, whatever the storage is.

Structure

The client only calls hasNext() and next(). How the stations are stored stays a private detail of the iterator
The client only calls hasNext() and next(). How the stations are stored stays a private detail of the iterator

Two parts:

  • The iterator: keeps the current position and exposes hasNext() and next().
  • The collection: creates iterators for itself, and can hide any internal structure.
class StationList {
  constructor(stations) {
    this.stations = stations;
    this.index = 0;
  }
  hasNext() { return this.index < this.stations.length; }
  next() {
    const station = this.stations[this.index];
    this.index += 1;
    return station;
  }
}

// const list = new StationList([98.5, 101.2, 104.9]);
// while (list.hasNext()) list.next(); → 98.5, 101.2, 104.9, in order

The loop is the same whether the stations live in an array, a linked list or a database cursor. Only the iterator changes.

Where it is used in real systems

JavaScript's for...of works on anything that follows the iterator protocol, so one loop serves every source
JavaScript's for...of works on anything that follows the iterator protocol, so one loop serves every source
Use caseWhat is being iterated
Arrays, Maps and SetsBuilt into the language through for...of.
GeneratorsValues produced lazily, one at a time.
Paginated APIsFetch the next page only when the loop needs it.
Database cursorsStream millions of rows without loading them all.
File and stream readingLine by line, or chunk by chunk.
Tree traversalDepth-first or breadth-first, without exposing the tree.
Infinite sequencesIDs, Fibonacci numbers, polling loops.

Iterators you already use

JavaScript builds the pattern into the language.

  • The iterator protocol. An object with next() returning { value, done } is an iterator. An object with a [Symbol.iterator]() method is iterable.
  • `for...of`, spread and destructuring all consume iterables: [...set], const [a, b] = map.
  • Generators (function*) build iterators with yield, without writing a class:
TypeScript
async function* allItems(fetchPage: (n: number) => Promise<string[]>) {
  for (let page = 1; ; page++) {
    const items = await fetchPage(page);
    if (items.length === 0) return;
    yield* items;
  }
}

for await (const item of allItems(fetchPage)) use(item);
  • `for await...of` iterates asynchronous sources, such as streams and paginated fetches.
  • Python uses __iter__ and __next__, and Java has Iterator<T> and the enhanced for loop.

Why lazy iteration matters

An iterator produces values on demand. That gives you two things an array cannot:

  • Memory stays flat. Reading a 5 GB file line by line uses a few kilobytes, because only the current line exists.
  • Work stops early. find on a lazy source stops at the first match. An array would already have paid for everything.

This is the reason to reach for iterators and generators when data is large or comes over the network.

When not to use it

  • A plain array is fine. For a small in-memory list, for...of over the array already is an iterator. Do not write your own.
  • You need random access. Iterators go forward. If you need the item at index 500, an array does that directly.
  • The collection changes while you iterate. Adding or removing items during a walk gives surprising results, and behavior differs between collections. Copy first, or avoid mutating.
  • Reuse. Most iterators are single-use. Once exhausted, they are done, and a second loop gets nothing. Make an iterable that creates a fresh iterator each time.

What interviewers ask

  • "What is the Iterator pattern?" A uniform way to traverse a collection without exposing how it is stored.
  • "How does JavaScript implement it?" Symbol.iterator, next() returning { value, done }, and for...of.
  • "What is a generator?" A function that can pause with yield, and produces an iterator automatically.
  • "Why is lazy iteration useful?" Constant memory and early exit on large or remote data.
  • "Iterator versus iterable?" An iterable can produce an iterator. An iterator is the object with next().

The mistake to avoid

An iterator that exposes or depends on the collection's internals, such as handing out the raw array or reading a length that can change mid-walk. The point is that the caller only sees hasNext() and next(). If callers can reach past the iterator to the storage, swapping the storage breaks them again.

The mechanism above, an object that remembers a position and hands out the next item, is the same in JavaScript, Python or Java. Only the syntax changes.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.