JavaScript Generators and Iterators, Explained
Generators are functions that pause and resume with the yield keyword, producing values lazily on demand instead of computing them all at once.
A JavaScript generator is a function that can pause its own execution with yield and resume later from exactly where it left off, producing a sequence of values one at a time instead of computing and returning them all at once. Generators are the mechanism behind JavaScript’s iterator protocol, and they show up anywhere you need lazy sequences, custom loops, or — historically — a cleaner way to write asynchronous code before async/await existed.
The iterator protocol
Before generators make sense, it helps to know what they implement. An iterable is any object with a Symbol.iterator method that returns an iterator — an object with a next() method returning { value, done }. Arrays, strings, Map, and Set are all built-in iterables, which is why for...of works on them:
const arr = [10, 20, 30];
const it = arr[Symbol.iterator]();
it.next(); // { value: 10, done: false }
it.next(); // { value: 20, done: false }
Writing this protocol by hand for a custom object is tedious — you have to manually track state between calls. Generators exist to make that trivial.
Writing a generator
A generator function is declared with function*, and calling it doesn’t run the body immediately — it returns a generator object, which is itself an iterator:
function* countUp(max) {
for (let i = 1; i <= max; i++) {
yield i;
}
}
const counter = countUp(3);
counter.next(); // { value: 1, done: false }
counter.next(); // { value: 2, done: false }
counter.next(); // { value: 3, done: false }
counter.next(); // { value: undefined, done: true }
Each call to next() runs the function body until it hits a yield, then pauses, preserving all local state — variables, loop position, everything — until next() is called again. Because generator objects are iterable, they work directly with for...of:
for (const n of countUp(3)) {
console.log(n); // 1, 2, 3
}
Why laziness matters
The core advantage of a generator is that values are produced on demand, not computed up front. This makes infinite sequences practical:
function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}
function take(iterable, count) {
const result = [];
for (const val of iterable) {
if (result.length >= count) break;
result.push(val);
}
return result;
}
take(naturals(), 5); // [1, 2, 3, 4, 5]
An eager version of naturals() would never return — it would try to build an infinite array before yielding anything. The generator version only does exactly as much work as the consumer asks for, which is the same principle behind lazy evaluation in data pipelines and streaming APIs.
Passing values back in
yield is a two-way channel. The value passed to next() becomes the result of the yield expression inside the generator:
function* echo() {
const first = yield "ready";
console.log("received:", first);
}
const gen = echo();
gen.next(); // { value: "ready", done: false }
gen.next("hello"); // logs "received: hello"
This is the mechanism that let libraries simulate async/await before it was native: a generator would yield a promise, and a runner function would resolve it and feed the result back in with next(value), repeating until the generator completed. Native async/await does the same thing under the hood but with syntax and a runner built into the language, which is why it fully replaced that pattern for asynchronous code.
Generators vs closures for custom iteration
A closure can also produce a stateful sequence — a function that remembers a counter between calls, for example — but it has to be called explicitly each time and doesn’t participate in for...of or spread syntax unless you also implement the iterator protocol by hand. A generator gets both the paused-state behavior of a closure and iterable-by-default integration with the rest of the language for free.
| Closures | Generators | |
|---|---|---|
| Pauses mid-execution | No | Yes, at each yield |
Works with for...of / spread | Only with manual Symbol.iterator | Automatically |
| Two-way value passing | Manual | Built in (next(value)) |
| Best for | Encapsulated state, memoization | Sequences, lazy pipelines, custom iteration |
Where generators show up today
Native async/await has taken over most of the asynchronous-flow use cases generators were originally introduced for, but generators remain the standard way to implement custom iterables — objects you want to work with for...of, array destructuring, or spread syntax — and they’re used internally by libraries that need cancellable, resumable computation, such as certain state-machine and effect-handling patterns in front-end frameworks. If you’re implementing a data structure and want it to behave like a native collection in a for...of loop, a generator method is almost always the simplest path there.
The takeaway
A generator is a function that pauses at yield and resumes on the next call to next(), and every generator object automatically implements the iterator protocol that powers for...of and spread syntax. Use generators when you need lazy, on-demand sequences or a clean way to make a custom object iterable — reach for a plain function or closure when you just need to encapsulate state without pausing mid-execution.
Tagged
Keep reading
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.
Takina · · 4 min read JavaScript Spread vs Rest Operators, Explained
The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.
Takina · · 4 min read ResizeObserver API Explained: Watching Element Size
The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.