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-waynext(value), infinite sequences. - Iterator helpers: lazy
map,filter,take,drop,toArrayES2025. - Async iteration:
Symbol.asyncIterator, async generators andfor await…ofES2018.
The iteration protocols
- An iterable is an object with a
[Symbol.iterator]()method that returns an iterator. - An iterator is an object with a
next()method that returns{ value, done }objects.
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), flatMap | toArray, 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'snext()returns{ value, done }.for…of, spread and destructuring all use them. function*plusyieldmakes iterators easy: they're lazy, they can be infinite, they pause and resume, andyield*delegates.breakcallsreturn(), so a generator'sfinallyblocks run, which is good for cleanup.- Iterator helpers (
map/filter/take/drop/toArray…) process sequences lazily and without arrays. async function*plusfor await…ofstreams asynchronous data such as pages, lines and events, andArray.fromAsynccollects 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.