Chapter 13 · Asynchronous JavaScript

Iterators & Generators

for…of, spread and destructuring all work through one small protocol introduced in ES6. Learn the protocol, write your own iterables with generators, chain the new iterator helpers lazily, and stream asynchronous data with for await.

You will learn

  • The iterable and iterator protocols, and which language features use them ES2015.
  • How to make your own objects iterable.
  • Generator functions: yield, yield*, two-way next(value), infinite sequences.
  • Iterator helpers: lazy map, filter, take, drop, toArray ES2025.
  • Async iteration: Symbol.asyncIterator, async generators and for await…of ES2018.

The iteration protocols

Built-in iterables: arrays, strings, Map, Set, typed arrays, arguments, NodeLists, and the results of entries(), keys() and values(). Plain objects are not iterable; use Object.entries(obj).

Consumers of the protocol
for…of · spread [...x] · array destructuring · Array.from · new Map(x) / new Set(x) · Promise.all(x) · yield* · Object.fromEntries(x)

Making an object iterable

Hand-written iterators are verbose. Generators, next, do the same job in a few lines.

Generators ES2015

A generator function (function*) returns a generator object, which is both an iterator and an iterable. Calling the function runs none of its body. Each next() runs the body up to the next yield, pauses there, and hands out the yielded value.

Generators as iterator implementations

Infinite and lazy sequences

A generator computes values only when asked, so infinite sequences are fine, as long as the consumer stops.

Two-way communication

next(value) sends a value into the generator: it becomes the result of the paused yield expression. (The first next() only starts the generator, so a value sent with it is ignored.)

☕ Java: a JavaScript iterable is Iterable<T>, and an iterator's next() folds hasNext()/next() into one call that returns { value, done }. Generators are like coroutines; Java has no direct equivalent (the closest is a Stream built from a Supplier).

Iterator helpers ES2025

Iterators now have their own lazy methods, so you can process huge or infinite sequences without first building arrays. They live on Iterator.prototype, and every built-in iterator and generator has them.

Lazy (return iterators)Eager (consume the iterator)
map, filter, take(n), drop(n), flatMaptoArray, reduce, forEach, some, every, find

⚠️ Arrays vs iterators: [1, 2, 3].map(f) is the array method (eager, returns an array). [1, 2, 3].values().map(f) is the iterator helper (lazy, returns an iterator). Iterators are single-use: once consumed, they're empty.

Async iteration ES2018

When each value arrives asynchronously, like pages from an API, lines from a file or chunks from a stream, use an async iterable: an object with [Symbol.asyncIterator](), whose next() returns a promise of { value, done }. An async function* creates one, and for await…of consumes it.

Reading a file line by line in Node

More examples

1. Pairwise and zip with generators

2. Batching items for bulk inserts

3. Walking a tree recursively with yield*

4. Cleanup on early exit with try/finally

5. Polling with an async generator and a limit

Recap

  • An iterable has [Symbol.iterator](); an iterator's next() returns { value, done }. for…of, spread and destructuring all use them.
  • function* plus yield makes iterators easy: they're lazy, they can be infinite, they pause and resume, and yield* delegates.
  • break calls return(), so a generator's finally blocks run, which is good for cleanup.
  • Iterator helpers (map/filter/take/drop/toArray…) process sequences lazily and without arrays.
  • async function* plus for await…of streams asynchronous data such as pages, lines and events, and Array.fromAsync collects it.

Exercises

Exercise 1 🟢 easy build it

Write a generator evens(limit) that yields 0, 2, 4, … below limit.

Show solution

Each yield hands out one value; spread pulls them all.

Exercise 2 🟢 easy predict the output

What does this print?

Show solution

[ 1, 2, 3 ] []. An iterator is single-use: the first spread consumes it. The array itself is iterable many times, because each spread asks it for a new iterator.

Exercise 3 🟢 easy fix the bug

This throws TypeError: book is not iterable. Fix it, so that it prints each key and value.

Show solution

Plain objects aren't iterable. Object.entries gives an array of [key, value] pairs.

Exercise 4 🟢 easy refactor it

Replace Array.from(map.values()).filter((p) => p > 1000).length with iterator helpers, without creating an intermediate array.

Show solution

The helpers run lazily, item by item, and reduce counts without materialising an array.

Exercise 5 🟡 medium predict the output

What does this print?

Show solution

1 20 5 true. The first next(5) only starts the generator, so the 5 is ignored and 1 is yielded. next(2) makes x = 2 and yields 20. next(3) makes y = 3 and returns 5. After that, it's done.

Exercise 6 🟡 medium build it

Make the Cart class iterable, yielding [id, qty] pairs, so that new Map(cart) and for (const [id, qty] of cart) both work.

Show solution

A generator method named [Symbol.iterator] makes the class iterable; yield* delegates to the private Map.

Exercise 7 🟡 medium build it · playground

Create playground/stream-orders.js with an async generator orderStream(count, ms) that yields fake orders { id, total } every ms milliseconds. Consume it with for await, keeping a running total, and stop once the total passes 5000 cents.

Show solution

break stops the async generator, so no further orders are produced or waited for.

Exercise 8 🟡 medium explain why

Why can an infinite generator be used safely with .take(5).toArray(), but not with [...gen()]?

Show solution

Spread keeps calling next() until done is true, which never happens, so it runs forever (and runs out of memory). take(5) is lazy: it pulls only five values and then stops asking, so the generator never runs further.

Exercise 9 🔴 hard predict the output

What does this print?

Show solution

cleanup, then a b. Array destructuring pulls only two values, then closes the iterator by calling return(), which runs the finally block, all before the console.log line executes.

Exercise 10 🔴 hard mini-project · playground

Create playground/pipeline.js: a lazy order-processing pipeline. Start from an async generator of raw CSV lines, then parse, skip invalid rows, keep only orders over 1000 cents, and group them into batches of 2. Write each stage as an async function* that takes an async iterable, and compose them like batch(filter(parse(lines))).

Show solution

Each stage pulls from the previous one on demand, so only one order is in flight at a time, however long the input is. The same shape works for real files and HTTP streams.

Official docs