Iterators & generators
The iteration protocol, making objects iterable, function* generators and async iteration.
for...of, the spread operator, destructuring, Array.from, Promise.all, new Map(...) — they all work on any iterable. In this lesson you'll learn the simple protocol behind that, make your own objects iterable, and meet generators: functions that can pause and resume. They're perfect for lazy sequences, paginated APIs and data streams.
The iteration protocol#
Two small interfaces:
- 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 }.
Let's drive an array's iterator by hand:
for...of does exactly this for you: get an iterator, call next() until done is true.
Built-in iterables include arrays, strings, Maps, Sets, arguments, NodeLists and typed arrays. Plain objects are not iterable:
Making your own iterable#
Because Range follows the protocol, every language feature that consumes iterables just works.
Generators: iterators made easy#
Writing next() by hand is fiddly. A generator function (function*) creates an iterator for you. Each yield produces a value and pauses the function until the next next() call:
Generators are iterable, so the Range class becomes three lines:
Lazy and infinite sequences#
Generators compute values on demand, so they can describe infinite sequences without running forever:
The for...of inside take stops pulling values after 10, so fibonacci never needs to finish.
Iterator helpers (ES2025)
Modern engines (Node 22+, current browsers) add map, filter, take, drop, flatMap, reduce, toArray, some, every, find and forEach directly to iterators — lazily:
Unlike array methods, no intermediate arrays are created — each value flows through the whole pipeline one at a time.
Delegating with yield*#
yield* yields every value from another iterable — handy for recursion:
Two-way communication (briefly)#
next(value) sends a value into the generator — it becomes the result of the paused yield expression. return() finishes the generator early and runs any finally blocks:
for...of calls return() automatically when you break out early, so cleanup code in finally always runs.
Async iteration#
For data that arrives over time — paginated APIs, file streams, websockets — use async generators (async function*) and consume them with for await...of:
The caller doesn't know or care about pagination; it just loops. Breaking early means page 3 is never requested.
Real APIs work the same way. With fetch, a GitHub-style paginated endpoint becomes:
Node streams are async iterables too:
And Array.fromAsync(asyncIterable) collects everything into an array.
When to use what#
Common mistakes#
- Expecting a generator to run when called — nothing happens until
next()(orfor...of). - Spreading an infinite generator:
[...fibonacci()]never finishes. Limit it withtake. - Reusing a finished generator — each generator object can be iterated only once. Call the function again for a fresh one.
- Using
for...ofon an async iterable (usefor await...of) orfor...inon any iterable.
What's next#
Next, sharpen a skill every developer needs daily: debugging JavaScript with the console, breakpoints and DevTools.
Check your understanding
Quick quiz
1.What makes an object usable in
for...of?2.What does calling a generator function
function* gen() {...}return?3.Given
function* g() { yield 1; yield 2; }, what is[...g()]?
Finished reading?
Mark this lesson complete to track your progress.