Lazy, pausable functions
Most functions run start to finish in one shot. Generators are different: they can
pause mid-execution and resume later, producing a sequence of values on demand. This
makes them ideal for lazy evaluation, infinite sequences, and streaming data without loading
everything into memory. Generators are built on JavaScript's iteration protocols, the same
machinery that powers for...of, spread, and destructuring. Understanding both gives you a
powerful, often-underused tool — and explains how iteration works throughout the language.
The iterator protocol
An iterator is any object with a next() method that returns { value, done }. Each
call advances the sequence; done: true signals the end.
const it = {
i: 0,
next() {
return this.i < 3
? { value: this.i++, done: false }
: { value: undefined, done: true } // terminator
}
}
it.next() // { value: 0, done: false }
it.next() // { value: 1, done: false }
This simple contract is what every iteration construct ultimately calls.
The iterable protocol
An iterable is an object with a [Symbol.iterator]() method that returns an iterator.
The iterable is the collection; the iterator is the cursor. for...of, spread, Array.from,
and destructuring all look for Symbol.iterator.
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let current = this.from
const last = this.to
return {
next: () => current <= last
? { value: current++, done: false }
: { value: undefined, done: true }
}
}
}
[...range] // [1, 2, 3]
for (const n of range) {} // works
Built-ins like arrays, strings, Map, and Set all implement this protocol — which is why
they work with for...of. Plain objects don't, which is why for...of {} throws.
Generators: protocols made easy
Writing iterators by hand is tedious. A generator function (function*) produces an object
that is both an iterator and an iterable, managing all the { value, done } bookkeeping for
you. Each yield pauses and emits a value.
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
g.next() // { value: 1, done: false }
[...gen()] // [1, 2, 3] spreadable directly
Calling a generator doesn't run its body — it returns a generator object. The body advances
only when you call next() (or iterate). This pause/resume is the entire point.
Rewriting range as a generator
Compare the verbose manual iterator above with the generator version — the difference is dramatic.
function* range(from, to) {
for (let i = from; i <= to; i++) yield i // all bookkeeping handled
}
[...range(1, 3)] // [1, 2, 3]
To make a class iterable, define [Symbol.iterator] as a generator method and yield the
elements — no next()/done plumbing required.
Lazy and infinite sequences
Because generators compute values only when asked, they can represent infinite sequences safely, as long as the consumer stops pulling.
function* naturals() {
let n = 1
while (true) yield n++ // infinite, but lazy
}
const it = naturals()
it.next().value // 1 — only this much computed
it.next().value // 2
The pitfall: [...naturals()] or a for...of without a break will loop forever. Always
bound consumption (a take(n) helper, a break, or destructuring a fixed count).
yield* — delegation
yield* delegates to another iterable, yielding all of its values inline. It's perfect for
composing generators or flattening structures.
function* inner() { yield 1; yield 2 }
function* outer() {
yield 0
yield* inner() // yields 1, then 2
yield 3
}
[...outer()] // [0, 1, 2, 3]
yield* works on any iterable (arrays, strings, Sets), making recursive flattening elegant:
yield* flatten(child) for nested data.
Two-way communication
Generators aren't just producers — they can receive values. The argument to next(value)
becomes the result of the yield expression the generator is paused on.
function* conversation() {
const name = yield 'What is your name?' // receives the next() argument
yield `Hello, ${name}!`
}
const c = conversation()
c.next().value // 'What is your name?'
c.next('Ada').value // 'Hello, Ada!' 'Ada' became `name`
Note the first next() argument is ignored — there's no paused yield yet to receive it.
This two-way channel enables coroutine-style control flow.
Early termination and cleanup
for...of automatically calls the generator's return() when you break or throw, running
any finally block — so resources get released.
function* withResource() {
try {
while (true) yield acquire()
} finally {
release() // runs even on early break
}
}
for (const x of withResource()) { if (done) break } // release() called
This makes generators a clean way to manage resources tied to iteration.
Single-use iterators
A generator object is a one-shot iterator — once exhausted, it stays done and won't restart.
const g = (function* () { yield 1; yield 2 })()
[...g] // [1, 2]
[...g] // [] already consumed
To re-iterate, expose a factory (a function returning a fresh generator) or a class whose
[Symbol.iterator] creates a new one each call.
Async generators
An async function* can await inside and yield asynchronously; you consume it with
for await...of. This is ideal for paginated APIs and streams where each chunk arrives over
time.
async function* pages(url) {
let next = url
while (next) {
const res = await fetch(next) // await inside
const data = await res.json()
yield data.items
next = data.nextPage
}
}
for await (const items of pages('/api/items')) {
process(items)
}
Async generators combine lazy iteration with asynchrony — streaming data on demand, one awaited chunk at a time.
When to use generators
Reach for generators when you need laziness (compute on demand), infinite or streaming
sequences, memory-bounded processing of huge data, or two-way coroutine control.
For small, fully-consumed collections, plain arrays and array methods are simpler and faster to
index. Generators add per-next() overhead and can't be randomly accessed or reused — so use
them where their unique powers actually pay off.
Key takeaways
- An iterator has
next()returning{ value, done }; an iterable has[Symbol.iterator]()returning an iterator — the basis offor...of, spread, destructuring. - Generators (
function*withyield) produce objects that are both, handling all protocol bookkeeping and pausing/resuming on demand. - They enable lazy and infinite sequences — but bound consumption or you'll loop forever.
yield*delegates to other iterables;next(value)enables two-way communication.- Generators are single-use; expose a factory to re-iterate.
for...oftriggers cleanup viareturn(). - Async generators +
for await...ofstream asynchronous data like paginated APIs.
Generators turn iteration into something you control step by step — lazy, composable, and memory-friendly — and reveal the protocols that make all JavaScript iteration tick.