Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 20

Iterators and Generators

Prerequisites assumed: fluency with objects, closures, arrays and their iteration methods, the prototype chain, and the event loop. This chapter examines the iteration protocol at the language level, then treats generators as compiler-managed coroutines — including the failure modes that show up in
Level: AdvancedLength: In depth4 min read

Read at another depth

Iterators and Generators

Prerequisites assumed: fluency with objects, closures, arrays and their iteration methods, the prototype chain, and the event loop. This chapter examines the iteration protocol at the language level, then treats generators as compiler-managed coroutines — including the failure modes that show up in production code.

The iteration protocol

JavaScript iteration is defined by two conventions, not by classes. Iterable is any object with a [Symbol.iterator] method that returns an iterator. An iterator is any object with a next() method returning { value, done }. The two protocols are deliberately decoupled: one iterable can produce fresh iterators repeatedly; an iterator itself may or may not be iterable.

Every consuming construct — for...of, spread, array destructuring, Array.from, yield*, new Map(iterable), Promise.all — calls [Symbol.iterator] once and then calls next() until done is truthy. The value of the final done: true result is ignored by all built-in consumers, which is why return-less generator exits yield undefined implicitly.

Code example in javascript
const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

console.log([...range]); // [1, 2, 3]
for (const n of range) console.log(n); // 1, 2, 3

Because range implements [Symbol.iterator] as a fresh call per consumer, it is indefinitely re-iterable. This is a common bug source when authors instead return a shared iterator: a for...of loop that has consumed the sequence breaks every later consumer.

Iterators versus arrays: the actual trade-off

PropertyArrayIterator/generator
Random accessO(1)O(n) per advance
Re-iterableYesOnly if `[Symbol.iterator]` recreates it
BackpressureNone — fully materializedPull-based, on demand
Infinite sequencesImpossibleNatural
`map`/`filter`Eager, allocatesMust be composed manually or via iterator helpers

Generators as suspended functions

A generator function (function*) produces a generator object that is both an iterator and iterable (its [Symbol.iterator] returns this — so generators re-iterating themselves is a trap: a exhausted generator is empty forever).

The internal machinery is a suspended stack frame. On each next() call, the frame resumes at the last yield, runs to the next yield or return, and suspends. This is implemented as a state machine over the function body; the generator's lexical scope persists across suspensions like a closure's.

Code example in javascript
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1

Bidirectional communication

next(arg) is where generators diverge from ordinary iteration: arg becomes the value of the currently suspended yield expression. This turns a generator into a coroutine — a function you can push values into, not just pull from.

Code example in javascript
function* accumulator() {
  let total = 0;
  while (true) {
    const input = yield total;
    total += input;
  }
}

const acc = accumulator();
console.log(acc.next());        // { value: 0, done: false } — first next() only starts the body
console.log(acc.next(10).value); // 10
console.log(acc.next(5).value);  // 15

The first next() cannot receive a value because no yield is suspended yet; anything passed to the starting next() is silently discarded. This is a classic gotcha.

return() and the awkward contract

generator.return(v) forces the frame to behave as if a return v executed at the suspension point: pending finally blocks run, and the generator becomes permanently done. for...of calls return() when it exits early (break, throw), which is the mechanism that makes try/finally inside generators correct under early loop exit.

A subtlety: a generator currently executing a finally block ignores return() calls — the result is an object with done: true only after the block completes. And yield inside finally is legal, which can make cleanup observable to consumers in surprising orders.

throw() and error propagation

generator.throw(e) resumes the frame as if throw e occurred at the suspension point. If the generator has no matching catch, the exception propagates to the .throw() caller and the generator is done. If it catches the error, the generator resumes normally. Error handling inside a generator is therefore cooperative and testable:

Code example in javascript
function* guarded() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('caught inside:', e.message);
    yield 'recovered';
  }
}

const it = guarded();
it.next();               // 1
console.log(it.throw(new Error('boom')).value); // 'recovered'

yield* and delegation

yield* iterable delegates: it drives the inner iterable, forwarding every next(), throw(), and return() call transparently, and evaluates to the inner iterable's return value. The return-value forwarding is underused and worth internalizing — it makes generators composable in a way that mimics monadic chaining:

Code example in javascript
function* inner() {
  yield 1;
  yield 2;
  return 'inner-done';
}

function* outer() {
  const result = yield* inner();
  console.log(result); // 'inner-done'
  yield 3;
}

console.log([...outer()]); // [1, 2, 3]

Async generators and the event loop

async function* yields promises; the consumer must await each next(). The returned iterator is an async iterator, consumed with for await...of. Each next() returns a promise resolving to { value, done }, and the pull-based model gives you natural backpressure for streams — the consumer's pace throttles production, unlike pushing every chunk into an array.

Code example in javascript
async function* ticks(n) {
  for (let i = 1; i <= n; i++) {
    await new Promise((r) => setTimeout(r, 100));
    yield i;
  }
}

(async () => {
  for await (const t of ticks(3)) console.log(t);
})();

Gotcha: within one generator, yield and await must not mix in a plain generator, and for await...of on a source that throws will invoke return() on the async iterator — cleanup applies there too.

Decision criteria

  • Use an array when data is small, needs random access, or is iterated repeatedly.
  • Use a generator for lazy pipelines, infinite or on-demand sequences, or when composition via yield* clarifies structure.
  • Use an async generator over an event-emitter or stream API when the consumer should control pacing.
  • Avoid generators for hot paths with per-item work: resuming a generator frame costs more than a plain loop; measure before reaching for them.

Exercise

Why is a value passed to the first next() call on a fresh generator discarded?

Show answer

The first next() starts execution from the top of the function body; there is no suspended yield expression to receive the value, so the argument has no destination and is dropped.

Exercise

A for...of loop breaks out of a generator-driven sequence that holds an open file handle released in a finally block. Does the handle get released, and by what mechanism?

Show answer

Yes. On early exit the for...of construct calls the iterator's return() method, which resumes the generator as if return executed at the suspension point; that unwinds the frame, running the pending finally block and releasing the handle.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Iterators and Generators

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that