Iterators and Generators

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

for…​of, the spread operator (…​), and destructuring all work on arrays, strings, Set, and Map "for free," but none of that is a special case baked into the language for those specific types — it is all built on one general-purpose protocol that any object can opt into. This page explains that protocol from the ground up: how for…​of actually drives an object step by step, how to make a custom class participate in it, and how generator functions (function*/yield) give you a dramatically shorter way to write an iterator than implementing the protocol by hand.

The Iterator Protocol

Three distinct roles are involved in every iteration, and it helps to keep them separate:

  • An iterable is any object with a method named Symbol.iterator (a well-known symbol, not a string) that returns an iterator. Arrays, strings, Set, and Map are all iterables in this sense.

  • An iterator is any object with a next() method. Each call advances the iteration by one step and returns an iteration result.

  • An iteration result is a plain object with a value property (the item produced by this step) and a done boolean (true once there is nothing left to produce).

for…​of is syntactic sugar over calling [Symbol.iterator]() once to obtain an iterator, then calling next() repeatedly until done is true. Written out longhand, without the sugar:

let iterable = [10, 20, 30];
let iterator = iterable[Symbol.iterator]();

let result = iterator.next();
while (!result.done) {
  console.log(result.value); // 10, then 20, then 30
  result = iterator.next();
}

The diagram below shows that same request/response cycle from the caller’s perspective — for…​of (or the spread operator, or destructuring) is always the one driving, and the iterator is always the one responding:

sequenceDiagram participant Caller as for...of participant Iter as Iterator Caller->>Iter: [Symbol.iterator]() Iter-->>Caller: iterator object loop until done Caller->>Iter: next() Iter-->>Caller: { value, done: false } end Caller->>Iter: next() Iter-->>Caller: { value: undefined, done: true }
The built-in iterator types are themselves iterable — their Symbol.iterator method just returns this. That is what lets a partially consumed iterator be resumed with another for…​of or spread: let iter = [1, 2, 3][Symbol.iterator](); iter.next(); […​iter] yields [2, 3], picking up exactly where the first next() call left off.

Because a Map iterates [key, value] pairs, and those pairs destructure naturally in a for…​of loop’s declaration, iterating a map’s entries reads almost like a dedicated syntax even though it is just the general protocol at work:

let scores = new Map([["alice", 92], ["bob", 81]]);

for (const [name, score] of scores) {
  console.log(name, score);
}

Writing a Custom Iterable

Any class can opt into for…​of, spreading, and destructuring by implementing a [Symbol.iterator]() method that returns an object with a next() method. The classic example is a numeric range that does not store its members as an array — it computes each one on demand:

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  [Symbol.iterator]() {
    // Each call must return an independent iterator, so the cursor
    // lives in a variable captured by this closure, not on `this`.
    let next = Math.ceil(this.from);
    let last = this.to;

    return {
      next() {
        return next <= last ? { value: next++, done: false } : { value: undefined, done: true };
      },
      // Making the iterator itself iterable is a small convenience
      // that lets it be resumed directly with for...of or spread.
      [Symbol.iterator]() {
        return this;
      },
    };
  }
}

[...new Range(1, 5)]; // => [1, 2, 3, 4, 5]
for (const n of new Range(1, 3)) console.log(n); // 1, then 2, then 3

Tracking the cursor (next) in a local variable rather than a property on Range matters: two independent for…​of loops over the same Range instance must not interfere with each other, and each call to [Symbol.iterator]() returns a fresh closure with its own cursor.

The same pattern — return an object with a next() method that either produces a value or reports done — also works for iterables that filter or transform another iterable lazily, without ever materializing an intermediate array:

function filter(iterable, predicate) {
  let iterator = iterable[Symbol.iterator]();
  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      for (;;) {
        let item = iterator.next();
        if (item.done || predicate(item.value)) return item;
      }
    },
  };
}

[...filter(new Range(1, 10), (n) => n % 2 === 0)]; // => [2, 4, 6, 8, 10]

Laziness like this is the main payoff of implementing the protocol directly rather than always building an array up front: filter() above only ever asks its source for the next value when its own caller asks it for one, so an infinite or very large source can be filtered without ever holding more than one element in memory at a time.

return(): cleaning up an interrupted iteration

for…​of does not always run to completion — a break, an early return, or an uncaught exception can all stop it partway through. If the iterable held onto a resource (an open file handle, a database cursor) that needs closing, that cleanup must not depend on the iteration finishing normally. For exactly this case, an iterator may implement an optional return() method: whenever iteration stops before next() reports done: true, the interpreter checks for a return() method and, if one exists, calls it with no arguments so the iterator gets a chance to release whatever it was holding:

function readLines(source) {
  let connection = source.openConnection();
  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      let line = connection.readLine();
      return line === null ? { value: undefined, done: true } : { value: line, done: false };
    },
    return(value) {
      connection.close(); // runs even if the loop exits early
      return { value, done: true };
    },
  };
}

for (const line of readLines(logFile)) {
  if (line === "STOP") break; // triggers return(), closing the connection
  console.log(line);
}

return() must itself return an iteration result object — its value/done properties are ignored, but returning a non-object is an error.

Generator Functions

Implementing Symbol.iterator, a next() method, and the { value, done } bookkeeping by hand works, but it is a lot of ceremony for what is conceptually a simple idea: "produce these values, one at a time." Generator functions — declared with function* instead of function — exist to remove that ceremony. Calling a generator function does not run its body; it returns a generator object, which is both an iterator and an iterable. Each call to that generator’s next() runs the function body from wherever it last stopped, up to the next yield expression, and the yielded value becomes the value of that next() call’s result:

function* oneDigitPrimes() {
  yield 2;
  yield 3;
  yield 5;
  yield 7;
}

let primes = oneDigitPrimes(); // body has not run yet
primes.next().value; // => 2
primes.next().value; // => 3

[...oneDigitPrimes()]; // => [2, 3, 5, 7]
for (const p of oneDigitPrimes()) console.log(p); // 2, 3, 5, 7

The can appear on a function expression, and on a method in a class body or object literal (using the same shorthand method syntax as a regular method, just with before the name):

const seq = function* (from, to) {
  for (let i = from; i <= to; i++) yield i;
};

class Range {
  constructor(from, to) {
    this.from = from;
    this.to = to;
  }

  // A generator-based Symbol.iterator is far shorter than the
  // hand-written version shown earlier in this page.
  *[Symbol.iterator]() {
    for (let x = Math.ceil(this.from); x <= this.to; x++) yield x;
  }
}
There is no arrow-function form of a generator — const g = () ⇒ { …​ } is not valid syntax. A generator must be declared with function, a function* expression, or the *methodName() shorthand.

A generator’s `yield`s do not have to be a fixed, hardcoded list — they can come from a loop or from real computation, including an unbounded one, which is where generators start to earn their keep:

function* fibonacciSequence() {
  let a = 0, b = 1;
  for (;;) {
    yield b;
    [a, b] = [b, a + b];
  }
}

// Safe: for...of only pulls as many values as the loop body consumes.
function nthFibonacci(n) {
  for (const f of fibonacciSequence()) {
    if (n-- <= 0) return f;
  }
}
nthFibonacci(10); // => 55

Because a generator only computes a value when next() actually asks for one, fibonacciSequence() above never has to decide in advance how many terms to produce — spreading it ([…​fibonacciSequence()]) would run forever, but consuming it through a bounded for…​of (as nthFibonacci does) is perfectly safe. A take() generator makes that bound reusable instead of ad hoc:

function* take(n, iterable) {
  let iterator = iterable[Symbol.iterator]();
  while (n-- > 0) {
    let next = iterator.next();
    if (next.done) return;
    yield next.value;
  }
}

[...take(5, fibonacciSequence())]; // => [1, 1, 2, 3, 5]

Delegating with yield*

A generator often needs to yield every value of some other iterable in turn — forwarding, rather than producing values itself. Written naively, that is a nested loop:

function* sequence(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

yield* collapses the inner loop: it iterates the given iterable itself and yields each of its values in turn, so the generator body only has to say what it is delegating to:

function* sequence(...iterables) {
  for (const iterable of iterables) {
    yield* iterable;
  }
}

[...sequence("ab", oneDigitPrimes())]; // => ["a", "b", 2, 3, 5, 7]

yield* accepts any iterable — including another generator — which is what makes recursive generators possible, e.g. flattening a tree structure by having each node’s generator yield* into its children’s generators. yield and yield* are only legal directly inside a function* body: reaching for Array.prototype.forEach() with a yield* inside its callback does not work, because that callback is an ordinary function, not a generator, even though it is nested inside one.

Generator Return Values

Like any function, a generator function can return a value rather than just falling off the end. That return value shows up on the final call to next(), alongside done: true — but it is invisible to for…​of and the spread operator, both of which stop as soon as they see done: true and never look at that last value:

function* oneAndDone() {
  yield 1;
  return "finished";
}

[...oneAndDone()]; // => [1] -- the return value never appears here

let g = oneAndDone();
g.next(); // => { value: 1, done: false }
g.next(); // => { value: "finished", done: true }
g.next(); // => { value: undefined, done: true } -- calling again after done is a safe no-op

yield as a Two-Way Channel

yield is not only a way to send values out of a generator — it is an expression, and the value it evaluates to is whatever gets passed into the next call to next(). That makes a generator and its caller two cooperating, independently paused threads of execution that pass values back and forth in both directions:

function* smallNumbers() {
  let a = yield 1; // pauses here; a gets whatever the *next* next() call passes in
  let b = yield 2;
  let c = yield 3;
  return [a, b, c];
}

let g = smallNumbers();
g.next();      // => { value: 1, done: false } -- argument to this first call is discarded
g.next("x");   // => { value: 2, done: false } -- "x" becomes a
g.next("y");   // => { value: 3, done: false } -- "y" becomes b
g.next("z");   // => { value: ["x", "y", "z"], done: true } -- "z" becomes c

The asymmetry in that first call matters: it is what actually starts the generator running up to its first yield, so whatever argument is passed to it has nowhere to go and is simply discarded.

return() and throw()

A running generator can also be steered from the outside without waiting for it to yield on its own, via two methods every generator object exposes:

  • generator.return(value) makes the paused yield expression behave as though a return value; statement had appeared at that exact point — the generator’s done becomes true, value becomes the returned value, and any finally block wrapping the paused code still runs. This is the generator counterpart to the iterator return() method described above: use a try/finally around the generator body to guarantee cleanup runs however the generator stops, since a generator’s return() cannot be overridden the way a hand-written iterator’s next()/return() pair can.

  • generator.throw(error) makes the paused yield expression behave as though throw error; had appeared there. If the generator body wraps that point in a try/catch, it can catch and handle the injected error instead of letting it propagate — a way to send an out-of-band signal (e.g. "reset", "cancel") into a running generator, not just a way to abort it.

function* countingUp() {
  let n = 0;
  try {
    for (;;) {
      try {
        yield n++;
      } catch (resetSignal) {
        n = 0; // throw() was used as a "reset" signal, not a fatal error
      }
    }
  } finally {
    console.log("cleanup: counter stopped"); // runs on return() too
  }
}

When a generator delegates to another iterable with yield*, calls to next(), return(), and throw() on the outer generator all forward to the delegated-to iterable’s own next()/return()/throw() methods (if it defines them), so cleanup and signaling propagate correctly through a chain of delegated generators, not just the outermost one.

Generators and Async Iteration

Everything on this page describes synchronous iteration: next() always returns its result immediately. Generators historically were also pressed into service to simulate asynchronous, sequential-looking code (pause at a yield, resume later once an asynchronous operation completes) — but doing that by hand is intricate and easy to get subtly wrong. The language now has a dedicated, purpose-built protocol for this instead: async generators (async function*) and for await…​of, which layer Promise-based waiting directly onto the same { value, done } shape described in this page. See Asynchronous JavaScript for how await, for await…​of, and async generators relate to the synchronous iterator protocol covered here, and why they — not hand-rolled generator-based coroutines — are the current, idiomatic way to express asynchronous sequences of values.