Beginner11 min readDesign Patternslive prototype

Iterator

Give clients one standard way to walk through a collection — `next()` and `hasNext()` — without ever exposing how the collection stores its items. Two music apps can keep your playlist in an array or in a linked list of nodes; you press *next song* the same way on both, because each hands you an **iterator** that knows its own way around.

The idea

What it is

Iterator answers a question every program hits eventually: how do I walk through the items in a collection without caring how the collection stores them? Its intent in one sentence: give a standard way to traverse a collection, one element at a time, without exposing its internal representation. The client sees only two tiny operations — hasNext() ("is there another one?") and next() ("give it to me and move forward") — and the collection keeps its storage details private.

Picture your music playlist on two different apps. App one stores the songs in a plain array; app two stores them in a linked list of nodes, each pointing at the next. Internally they could not be more different — indexed slots versus pointer-chasing. But you, the listener, press next song the exact same way on both. That's because each app hands you an iterator: a small object that knows how to walk its structure and remembers where you are. The traversal knowledge lives in the iterator, not in you.

The one sentence to remember

An Iterator moves the how do I walk this? question out of the client and into a small object with next() / hasNext(). Swap the collection's storage from array to linked list to tree — the client's loop doesn't change a single character.

You already use this every day

Every for...of in JavaScript, every for x in ... in Python, every enhanced for in Java is the Iterator pattern running under the hood. The loop syntax quietly asks the collection for an iterator and calls next() until it's exhausted.

Mechanics

How it works

The three moving parts

The classic pattern has three participants, and each one has exactly one job:

  • Iterator interface — the tiny contract every traversal object promises: next() returns the current element and advances, hasNext() says whether anything is left. This is all the client ever sees.
  • Concrete Iterator — one class per storage style, holding the cursor (current position). An array iterator keeps an index and does i++; a linked-list iterator keeps a node reference and does node = node.next. Same interface, totally different walk.
  • Iterable / Collection — the playlist itself. It exposes a createIterator() method that builds a fresh iterator positioned at the start, and never leaks its internal array or head node. (Notice createIterator() is itself a small [[factory-method]].)
typescript
interface SongIterator {
  hasNext(): boolean;          // anything left?
  next(): string;              // return current song, move forward
}

class ArrayPlaylist {
  private songs: string[] = ["Neon Sky", "Midnight Run", "Paper Moon"];

  createIterator(): SongIterator {
    let i = 0;                              // the cursor lives HERE
    return {
      hasNext: () => i < this.songs.length,
      next: () => this.songs[i++],          // hop the index
    };
  }
}

// The client never sees the array — just the two methods:
const it = new ArrayPlaylist().createIterator();
while (it.hasNext()) console.log(it.next());

The key detail: the cursor lives in the iterator, not in the collection. That's why you can call createIterator() twice and get two independent walks over the same playlist — each iterator remembers its own position. One part of your app can be halfway through the playlist while another part starts from the top, and neither disturbs the other. The prototype's + 2nd cursor button shows exactly this.

Different walks, same contract

Because the traversal logic is boxed inside the iterator, the order of iteration becomes a pluggable choice. The same playlist can hand out different iterators without changing the client loop at all:

  • Forward — the everyday case: first song to last.
  • Reverse — same songs, walked back-to-front; the client loop is identical.
  • Filtered — an iterator that skips as it goes, e.g. only songs over 3 minutes.
  • Tree in-order — for hierarchical structures (think a [[composite]] of folders and playlists), the iterator can flatten a whole tree into a simple sequence, hiding the recursion from the client.

Modern languages bake this in

Iterator is so useful that every mainstream language absorbed it into the language itself: Java has the Iterator interface and Iterable (which powers the for-each loop), Python has __iter__ / __next__ with StopIteration, JavaScript has Symbol.iterator driving for...of and the spread operator, and C++ has begin() / end() powering range-for. So in day-to-day work you rarely hand-roll the GoF classes — instead you implement your language's iterator protocol on your own types, and instantly every built-in loop, spread, and library function knows how to walk them.

Don't mutate a collection mid-iteration

The classic Iterator trap: adding or removing items from a collection while an iterator is walking it. The cursor's notion of "position" goes stale — you can skip elements, visit one twice, or crash. Java fails fast with a ConcurrentModificationException; Python and JavaScript may just silently misbehave. Rule of thumb: finish the walk first, collect the changes, apply them after — or use the iterator's own remove() if the language offers one.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

One playlist of 5 songs, stored two completely different ways: the left card is an ARRAY (contiguous slots [0..4]), the right card is a LINKED LIST (scattered nodes chained by pointers). Each side exposes the exact same two things: a ▶ next() button and a hasNext? chip. Press ▶ next() and watch the cursor advance — the array hops to the next index while the list follows a pointer arrow, yet the same song title floats up from both. ▶ next() on both drives the two collections with one identical client action. When a cursor runs out, its chip flips to ✗ and further presses just shake — hasNext() told you to stop. Hit + 2nd cursor to drop a second, independently-colored cursor on the array and prove that two iterations can walk the same collection at the same time without colliding. ↺ Reset starts over.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Same button, different walk

Press '▶ next()' under the ARRAY card a couple of times, then press '▶ next()' under the LINKED LIST card. Watch the cursors: the array's highlight hops from slot [0] to [1] to [2] in a straight line, while the list's highlight jumps between scattered nodes by following the pointer arrows. Both float up the same song title. The client action is identical — the traversal knowledge is inside each iterator.

try 02

One client action drives both

Press '▶ next() on both' repeatedly until the songs run out. Both collections advance in lockstep from one identical call — this is the uniform interface at work. When a side is exhausted its 'hasNext?' chip flips from ✓ to ✗, and pressing its '▶ next()' again just shakes the card: hasNext() is the guard that tells you to stop before next() has nothing to give.

try 03

Two independent cursors

Press '↺ Reset', then '+ 2nd cursor'. A second, differently-colored cursor appears on the array at slot [0]. Advance cursor A three times with '▶ next()', then advance cursor B once with its own '▶ next() B' button. A sits at [3] while B sits at [1] — each iterator owns its position, so two simultaneous walks over one collection never collide.

In practice

When to use it — and what trips people up

Reach for an Iterator when...

  • You want to hide storage details — clients should walk the playlist without knowing (or depending on) whether it's an array, linked list, hash map, or tree underneath. You stay free to swap the structure later.
  • You need uniform traversal across different structures — one loop that works over anything walkable. Program to the iterator interface, not the concrete collection (see [[program-to-interfaces]]).
  • Several traversals must run at the same time — each iterator carries its own cursor, so a UI can show 'now playing' at position 7 while a prefetcher scans ahead from position 8.
  • The sequence is lazy or streaming — results from a database cursor, lines from a huge file, pages from an API. An iterator produces items one at a time, so you never need the whole collection in memory at once.

When NOT to bother

If you own a plain array, it's local to one function, and its representation will never change — just index it with a for loop. Wrapping songs[i] in a hand-rolled iterator class adds ceremony with zero payoff. The pattern earns its keep when storage details are worth hiding, not on every loop you write.

What it gives you

  • Decouples clients from storage — the collection can change from array to linked list to tree and every client loop keeps working unchanged.
  • One traversal contract for everything — a single hasNext()/next() loop (or for...of / for-each) works across wildly different structures, including lazy and infinite sequences.
  • Multiple simultaneous walks — each iterator owns its own cursor, so independent traversals over one collection never interfere with each other.
  • Simplifies the collection itself — traversal logic (even complex tree walks) moves out into iterator objects, keeping the collection's own interface small (single responsibility).

Common mistakes

  • Overkill for simple cases — a plain indexed loop over an array you own is shorter and clearer than defining iterator machinery.
  • Can be slower than direct access — going through next() calls adds indirection, and an iterator hides capabilities like 'jump straight to index 500' that the raw structure may offer.
  • Stale-cursor hazards — mutating the collection mid-iteration invalidates cursors (Java's ConcurrentModificationException; undefined behaviour with invalidated iterators in C++).
  • Extra classes to maintain when hand-rolled — each storage style needs its own concrete iterator; without language support the boilerplate adds up.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// A playlist stored as a LINKED LIST — but clients never see the nodes,
// because we implement JS/TS's native iterator protocol: Symbol.iterator.
class Node {
  constructor(public song: string, public next: Node | null = null) {}
}

class LinkedPlaylist implements Iterable<string> {
  private head: Node | null = null;
  private tail: Node | null = null;

  add(song: string): void {
    const node = new Node(song);
    if (this.tail) { this.tail.next = node; this.tail = node; }
    else { this.head = this.tail = node; }
  }

  // The iterator protocol: return an object with next() -> { value, done }.
  [Symbol.iterator](): Iterator<string> {
    let cursor = this.head;                   // cursor lives in the iterator
    return {
      next(): IteratorResult<string> {
        if (!cursor) return { value: undefined, done: true };  // "hasNext" = false
        const song = cursor.song;
        cursor = cursor.next;                 // follow the pointer
        return { value: song, done: false };
      },
    };
  }
}

const playlist = new LinkedPlaylist();
playlist.add("Neon Sky");
playlist.add("Midnight Run");
playlist.add("Paper Moon");

// The client walks it like any array — no idea it's a linked list:
for (const song of playlist) console.log("▶", song);

// Bonus: the protocol also unlocks spread and destructuring for free.
const [first] = playlist;      // "Neon Sky"
console.log([...playlist]);    // ["Neon Sky", "Midnight Run", "Paper Moon"]

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is the core intent of the Iterator pattern?

question 02 / 05

In the playlist analogy, why can you press 'next song' the same way on an array-backed app and a linked-list-backed app?

question 03 / 05

Where does the cursor (current position) live, and why does that matter?

question 04 / 05

You remove songs from a playlist while an iterator is halfway through walking it. What is the likely result?

question 05 / 05

How do modern languages relate to the classic GoF Iterator pattern?

0/5 answered