← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / Iterators & Generators

Iterators & Generators

The iteration protocol, custom iterables, generator functions, and lazy sequences in JavaScript.

What it is, in plain English: An iterator is any object with a next() method that returns {value, done} pairs. An iterable is any object that has a [Symbol.iterator]() method returning an iterator. Arrays, strings, Maps, Sets, and NodeLists are all iterables — that's why for...of works on them. A generator function (function*) is a special function that can pause mid-execution with yield, producing one value at a time. Generators are the easiest way to create custom iterables.
Where this bites you

Leaving a for...of loop early, with break, return or a thrown error, doesn't just stop the loop. It calls the iterator's return() method, and for a generator that means "finished for good". So if you pass the same generator to a helper like take() twice, the second call gets nothing, because the first call's break already closed it.

function* naturals(start = 0) {
  let n = start;
  while (true) {
    yield n++;
  }
}

function take(n, iterable) {
  const result = [];
  for (const val of iterable) {
    result.push(val);
    if (result.length >= n) break;     // break also CLOSES the generator
  }
  return result;
}

const ids = naturals(1);
console.log(take(3, ids));   // first page
console.log(take(3, ids));   // second page?
// [ 1, 2, 3 ]
// []

Iterators, iterables, and generator functions

You've used for...of loops to iterate over arrays and strings. But how does JavaScript know how to loop over them? The answer is the iteration protocol — a contract that any object can implement to become "loopable." Generator functions make implementing this contract easy.

The iteration protocol

An object is iterable if it has a [Symbol.iterator]() method that returns an iterator — an object with a next() method that returns { value, done } pairs.

// Arrays are built-in iterables:
const arr = [10, 20, 30];

// for...of uses the iterator protocol under the hood:
for (const n of arr) {
  console.log(n);   // 10, 20, 30
}

// You can call the protocol manually:
const iter = arr[Symbol.iterator]();
console.log(iter.next());  // { value: 10, done: false }
console.log(iter.next());  // { value: 20, done: false }
console.log(iter.next());  // { value: 30, done: false }
console.log(iter.next());  // { value: undefined, done: true }

Strings, Maps, Sets, NodeLists, and arguments objects are all built-in iterables. That's why for...of, spread (...), destructuring, and Array.from() all work on them.

Generator functions — the easy way to make iterables

Writing the iterator protocol by hand is verbose. Generator functions (function*) do it for you. A generator can yield a value and then pause — it resumes on the next .next() call.

// function* marks a generator function
function* countdown(from) {
  while (from > 0) {
    yield from;   // ← pause here, return 'from', wait for next .next()
    from--;
  }
  yield "Go!";
}

// A generator is automatically an iterable:
for (const value of countdown(3)) {
  console.log(value);  // 3, 2, 1, Go!
}

// Spread works too:
console.log([...countdown(3)]);  // [3, 2, 1, "Go!"]

A range generator — the most useful first generator

// Python has range(). JS doesn't — but you can write it:
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

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

console.log([...range(0, 10, 2)]);  // [0, 2, 4, 6, 8, 10]

// Works in destructuring:
const [first, second, third] = range(10, 50, 10);
console.log(first, second, third);  // 10 20 30

Infinite generators — safe because they're lazy

Generators produce values on demand — they don't calculate everything upfront. This makes infinite sequences safe (as long as you stop asking eventually):

// Infinite sequence — never ends, but that's fine:
function* naturals(start = 0) {
  let n = start;
  while (true) {
    yield n++;
  }
}

// Only take what you need:
function take(n, iterable) {
  const result = [];
  for (const val of iterable) {
    result.push(val);
    if (result.length >= n) break;
  }
  return result;
}

console.log(take(5, naturals()));      // [0, 1, 2, 3, 4]
console.log(take(5, naturals(100)));   // [100, 101, 102, 103, 104]

The take() above is fine when every call gets a fresh generator, as in take(5, naturals()). To keep pulling from the same generator, call next() yourself instead of looping with for...of. Calling next() never closes the generator, so the next call carries on where this one stopped:

function take(n, iterator) {
  const result = [];
  while (result.length < n) {
    const { value, done } = iterator.next();   // pull values without closing it
    if (done) break;
    result.push(value);
  }
  return result;
}

const ids = naturals(1);
console.log(take(3, ids));
console.log(take(3, ids));
// [ 1, 2, 3 ]
// [ 4, 5, 6 ]

Making your own iterable object

// Add [Symbol.iterator] to any object to make it iterable:
class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end   = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end   = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const nums = new NumberRange(1, 5);
console.log([...nums]);   // [1, 2, 3, 4, 5]

for (const n of nums) {
  console.log(n);  // 1, 2, 3, 4, 5
}

// Can iterate multiple times — each call to [Symbol.iterator]() creates a fresh iterator:
console.log([...nums]);   // [1, 2, 3, 4, 5] — works again
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.