Iterators and generators

The protocol behind for…of and spread, how to make your own objects iterable, and generator functions for lazy sequences.

  • Beginner
  • 8 min read
  • Updated 17 Oct 2026

What you will learn

  • Make an object iterable
  • Write a generator function
  • Build lazy sequences that compute values only when asked

Before you start

for...of works on arrays, strings, Maps, Sets and NodeLists, but not on plain objects. The reason is a small agreement called the iteration protocol. Learn it and you can make any object work with for...of, spread and destructuring, and you can write generators that produce values one at a time instead of building huge arrays.

The iteration protocol

The protocol has two parts:

  • An object is iterable if it has a method under the key Symbol.iterator that returns an iterator.
  • An iterator is an object with a next() method. Each call returns { value, done }. When done is true, the sequence is finished.

You can drive an array's iterator by hand:

JavaScript
const colours = ['red', 'green'];
const iterator = colours[Symbol.iterator]();

console.log(iterator.next()); // → { value: 'red', done: false }
console.log(iterator.next()); // → { value: 'green', done: false }
console.log(iterator.next()); // → { value: undefined, done: true }

for...of does exactly this: it asks for the iterator, calls next() until done is true, and gives you each value. Spread ([...x]), array destructuring, Array.from, new Map(x), new Set(x) and Promise.all(x) all use the same protocol.

Making an object iterable

Here is a range object that works with for...of:

JavaScript
const range = {
  from: 1,
  to: 4,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) {
  console.log(n);
}
// → 1
// → 2
// → 3
// → 4

console.log([...range]); // → [ 1, 2, 3, 4 ]
const [first, second] = range;
console.log(first, second); // → 1 2

Each call to range[Symbol.iterator]() creates a fresh iterator with its own current, so you can loop over range as many times as you like. Symbol.iterator is a well-known symbol; the symbols lesson later in the course explains them fully.

Writing next() by hand works, but tracking state between calls is fiddly. Generators do that bookkeeping for you.

Generator functions

A generator function is declared with function*. Calling it does not run the body. It returns a generator object, which is both an iterator and iterable. Each next() runs the body until the next yield, pauses there, and hands out the yielded value.

JavaScript
function* countdown() {
  console.log('starting');
  yield 3;
  yield 2;
  yield 1;
  console.log('finished');
}

const gen = countdown();          // Nothing logged yet.
console.log(gen.next());          // → starting
                                  // → { value: 3, done: false }
console.log(gen.next());          // → { value: 2, done: false }
console.log(gen.next());          // → { value: 1, done: false }
console.log(gen.next());          // → finished
                                  // → { value: undefined, done: true }

The function's local variables survive between calls because the function is paused, not finished. The same range becomes much shorter:

JavaScript
const range = {
  from: 1,
  to: 4,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) {
      yield n;
    }
  },
};

console.log([...range]); // → [ 1, 2, 3, 4 ]

*[Symbol.iterator]() is generator method syntax. It also works in classes:

JavaScript
class Playlist {
  #tracks = [];

  add(title) {
    this.#tracks.push(title);
    return this;
  }

  *[Symbol.iterator]() {
    yield* this.#tracks;
  }
}

const playlist = new Playlist().add('Intro').add('Theme');
console.log([...playlist]); // → [ 'Intro', 'Theme' ]

yield* delegates to another iterable, yielding each of its values in turn. Here it exposes the tracks without exposing the private array itself.

Lazy and infinite sequences

A generator only does work when asked for the next value. That makes infinite sequences safe, as long as you stop asking:

JavaScript
function* naturals() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

function* take(iterable, count) {
  if (count <= 0) return;
  let taken = 0;
  for (const value of iterable) {
    yield value;
    taken++;
    if (taken >= count) return;
  }
}

function* filter(iterable, test) {
  for (const value of iterable) {
    if (test(value)) yield value;
  }
}

const firstEvenSquares = take(
  filter(naturals(), (n) => n % 2 === 0),
  3
);

for (const n of firstEvenSquares) {
  console.log(n * n);
}
// → 4
// → 16
// → 36

naturals() never builds an array. Each value flows through filter and take one at a time, and when take returns, the loop ends. Never spread an infinite generator ([...naturals()]): it would try to build an infinite array and hang.

This style is useful for large or expensive data: reading records one at a time, generating IDs, or walking a tree.

JavaScript
function* walk(node) {
  yield node.name;
  for (const child of node.children ?? []) {
    yield* walk(child);
  }
}

const menu = {
  name: 'Home',
  children: [
    { name: 'Courses', children: [{ name: 'JavaScript' }, { name: 'CSS' }] },
    { name: 'About' },
  ],
};

console.log([...walk(menu)]);
// → [ 'Home', 'Courses', 'JavaScript', 'CSS', 'About' ]

Recursion with yield* turns a tree walk into a flat sequence that a caller can stop early.

Stopping early and cleaning up

When a for...of loop exits early with break, return or an exception, it calls the iterator's return() method. For a generator, that resumes the paused function as if a return statement were at the yield, so any finally block runs:

JavaScript
function* readLines() {
  console.log('open file');
  try {
    yield 'line 1';
    yield 'line 2';
    yield 'line 3';
  } finally {
    console.log('close file');
  }
}

for (const line of readLines()) {
  console.log(line);
  if (line === 'line 2') break;
}
// → open file
// → line 1
// → line 2
// → close file

This is how generators release resources reliably. A generator object can only be iterated once: after it finishes or is closed, further next() calls return { value: undefined, done: true }.

Sending values in

next(value) can also pass a value back into the generator; it becomes the result of the paused yield expression. The first next() call only starts the generator, so a value passed to it is ignored.

JavaScript
function* runningTotal() {
  let total = 0;
  while (true) {
    const amount = yield total;
    total += amount;
  }
}

const tally = runningTotal();
tally.next();                     // Start: runs to the first yield.
console.log(tally.next(5).value); // → 5
console.log(tally.next(10).value); // → 15

Two-way generators are powerful but harder to read. In everyday code, use generators to produce sequences and reach for this only when it clearly simplifies something.

Iterator helpers

ES2025 added iterator helpers: methods such as map, filter, take, drop, flatMap, reduce, toArray, some, every and find directly on iterators, so the take and filter functions above have built-in equivalents:

JavaScript
const result = naturals()
  .filter((n) => n % 2 === 0)
  .map((n) => n * n)
  .take(3)
  .toArray();

console.log(result); // → [ 4, 16, 36 ]

They are lazy, like the hand-written versions. They are newer than the rest of this lesson: they are not in Node.js 20 and some browsers added them only recently, so check MDN's compatibility table and keep your own small helpers as a fallback.

Summary

  • An iterable has a Symbol.iterator method that returns an iterator; an iterator has next() returning { value, done }.
  • for...of, spread, destructuring, Array.from, Map, Set and Promise.all all use this protocol.
  • function* creates generators that pause at each yield and keep their local state.
  • yield* delegates to another iterable, which makes recursive walks simple.
  • Generators are lazy, so infinite sequences are fine if you stop asking for values.
  • Exiting a loop early calls return(), which runs the generator's finally blocks.

Further reading