Back to JavaScript tutorials
Advanced14 min read

Iterators & Generators

Build lazy sequences and custom iteration protocols with iterators, generators, and async generators.

The Iterable Protocol

An object is iterable when it implements `[Symbol.iterator]()` returning an iterator. Iterators must provide `next()` returning `{ value, done }`. Built-in iterables include Array, String, Map, and Set.

`for...of` loops consume iterables. Spread and destructuring also use the iterator protocol under the hood, which is why you can spread Sets and Maps into arrays.

  • `Symbol.iterator` makes objects iterable
  • `next()` returns `{ value, done }`
  • for...of, spread, destructuring use iterables
const range = {
  *[Symbol.iterator]() {
    for (let i = 0; i < 3; i++) yield i;
  }
};
for (const n of range) console.log(n);

Generator Functions

Generator functions (`function*`) return generator objects that are both iterators and iterables. `yield` pauses execution and produces a value; the generator resumes when `next()` is called again.

Generators enable lazy evaluation — infinite sequences, paginated API consumption, and tree traversal without building full arrays in memory. Each step computes the next value on demand.

  • `function*` returns a generator
  • `yield` pauses and produces values
  • Lazy evaluation for large or infinite sequences
function* ids() {
  let i = 0;
  while (true) yield ++i;
}
const gen = ids();
gen.next().value; // 1

Delegating and Passing Values

`yield*` delegates iteration to another iterable, flattening nested generator composition. `next(value)` sends values back into the generator, enabling two-way communication rarely needed in application code but used in coroutine-style libraries.

`return()` and `throw()` on iterators allow early termination and error injection. Generator cleanup runs in `finally` blocks inside the generator when iteration ends.

  • `yield*` delegates to another iterable
  • `next(value)` sends data into generator
  • Use try/finally inside generators for cleanup
function* combined() {
  yield* [1, 2];
  yield* [3, 4];
}

Async Generators and for await...of

Async generators (`async function*`) yield Promises and work with `for await...of` to consume async iterables page by page. Ideal for streaming API results, reading file chunks, and database cursors.

Backpressure matters in production streams — consume at the rate you can process and abort when consumers disconnect. Node.js readable streams implement async iteration in modern versions.

  • `async function*` for async sequences
  • `for await...of` consumes async iterables
  • Handle backpressure and abort signals
async function* fetchPages(url) {
  let page = 1;
  while (true) {
    const data = await fetch(`${url}?page=${page}`).then(r => r.json());
    if (!data.length) return;
    yield* data;
    page++;
  }
}

Practical Use Cases

Use iterators when exposing collections from custom data structures without allocating intermediate arrays. Use generators for state machines where each yield represents a state transition readable in one function.

Most application code relies on built-in iterables. Reach for custom iterators when building libraries, parsing pipelines, or streaming large datasets — not for everyday array mapping where `.map()` is clearer.

  • Custom collections benefit from iterators
  • Generators for readable state machines
  • Prefer array methods for simple transforms

Get In Touch


Ready to discuss your next project? Drop me a message.