Generators are functions that can pause and resume. They produce a sequence of values on demand instead of building a whole array up front. This lesson covers the iteration protocol, the function* and yield syntax, and the practical patterns you will use in real code.
The iteration protocol
An object is iterable when it has a method whose key is Symbol.iterator that returns an iterator. An iterator has a next() method that returns an object with value and done. for-of, the spread operator, and destructuring all call this protocol under the hood.
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let i = this.from;
const last = this.to;
return {
next() {
if (i <= last) return { value: i++, done: false };
return { value: void 0, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3, 4, 5]
Generator functions with function*
A function declared with function* is a generator function. Calling it returns a generator object that is both iterable and an iterator. yield pauses the function and sends a value out. The next call to next() resumes right after the yield.
function* countdown(n) {
while (n > 0) {
yield n;
n -= 1;
}
}
for (const x of countdown(5)) {
console.log(x);
}
// 5 4 3 2 1
Sending values back with next(value)
next(value) sends a value into the generator. The value becomes the result of the current yield expression. This is the foundation of coroutines and pipelines.
function* runningAverage() {
let total = 0;
let count = 0;
while (true) {
const value = yield count ? total / count : 0;
total += value;
count += 1;
}
}
const avg = runningAverage();
avg.next(); // prime
console.log(avg.next(10).value); // 10
console.log(avg.next(20).value); // 15
console.log(avg.next(30).value); // 20
yield* delegates to another iterable
yield* yields every value from another iterable or generator. Use it to compose generators and to flatten nested data.
function* gen1() { yield 1; yield 2; }
function* gen2() { yield* gen1(); yield 3; yield 4; }
console.log([...gen2()]); // [1, 2, 3, 4]
function* flat(nested) {
for (const item of nested) {
if (Array.isArray(item)) yield* flat(item);
else yield item;
}
}
console.log([...flat([1, [2, 3, [4, 5]], 6])]);
// [1, 2, 3, 4, 5, 6]
Async generators
Combine async iteration with generator syntax by declaring async function* and using yield and await together. Async generators are the right tool for streaming data from a paginated API or a database cursor.
async function* paginate(fetchPage, pageSize = 100) {
let page = 0;
while (true) {
const items = await fetchPage(page, pageSize);
if (items.length === 0) return;
for (const item of items) yield item;
if (items.length < pageSize) return;
page += 1;
}
}
async function consume() {
for await (const item of paginate(myFetcher)) {
console.log(item);
}
}
Summary
Generators are the right tool for lazy sequences. Use function* and yield to pause and resume. yield* delegates to another iterable. async function* combines async iteration with generator syntax for streaming. The next lesson covers the DOM and events, the part of the language that runs in every browser.
Common pitfalls and how to avoid them
The most common bug with async generators is forgetting to call next() before reading values. A generator does not start running until next() is invoked, and the first call primes the function up to the first yield. The return value of next() is a result object, not the yielded value itself. Read result.value and ignore result unless you have a reason to inspect it.
function* letters() {
yield "a";
yield "b";
yield "c";
}
const it = letters();
console.log(it.next().value); // a (first call primes the generator)
console.log(it.next().value); // b
console.log(it.next().value); // c
console.log(it.next().done); // true
Another trap is treating a generator like an array. Each generator is a one-shot iterator. Once it is exhausted, you cannot iterate it again. If you need to iterate the same sequence multiple times, build a generator function and call it again for each iteration. Materials are reproducible; their iterators are not.
function* range(n) {
for (let i = 0; i < n; i++) yield i;
}
const r = range(3);
console.log([...r]); // [0, 1, 2]
console.log([...r]); // [] -- already exhausted
console.log([...range(3)]); // [0, 1, 2] -- call again
Best practices for production code
Use generators when the data is large or the source is slow. Reading a billion-line log file should not allocate an array of a billion strings; a generator can yield lines one at a time and keep memory flat. Use them when you want to express a pipeline: one generator transforms another and the consumer pulls at its own pace. Avoid them when a simple array or a plain function would do, because the cognitive overhead of paused functions is real. If you find yourself reaching for return inside a generator more than once, a regular function with an early return may be clearer.
// Lazy line stream from a fetch response
async function* linesFromUrl(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) {
if (buffer) yield buffer;
return;
}
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("
");
buffer = parts.pop();
for (const line of parts) yield line;
}
}
Comparison with Python generators
Python and JavaScript both have generators, and the mental model is similar. Python uses yield and yield from, JavaScript uses yield and yield*. Python generators can be sent to with send(value), and so can JavaScript generators. Python's StopIteration maps to JavaScript's done: true. The biggest practical difference is that JavaScript's async generators are first-class, while Python needs the asyncio library and the async def and yield syntax pair. If you already know one language's generators, the other comes quickly.