Key Takeaways
- Calling a generator function returns a lazy iterator; its body starts executing when you call
next(). yieldsends a value out, while a laternext(value)supplies the result of the suspendedyieldexpression.for...ofand spread consume yielded values, not the generator's final return value.- Synchronous generators don't await Promises. Prefer async functions for ordinary asynchronous work and async generators for asynchronously produced sequences.
JavaScript generators are functions that can pause at yield and resume when their iterator is advanced. They're useful for lazy sequences, custom iteration, and caller-controlled execution—not as a replacement for Promises or async/await.
Generator Functions vs Regular Functions
An ordinary synchronous function runs until it returns or throws; its caller can't suspend it at an arbitrary statement. A generator marks explicit suspension points with yield and preserves its local variables between resumptions.
Generator function syntax
Declare a generator with function*. Unlike an ordinary function call, calling it creates a Generator object without executing its body.
function* myGenFunction() {
yield "ready";
}
const starter = myGenFunction();
console.log(starter.next()); // { value: "ready", done: false }The asterisk changes how the function is called and consumed; yield identifies where execution can pause. If you need a syntax refresher, start with JavaScript function declarations and expressions.
How JavaScript yield and next() Work Together
An iterator is an object you advance with next(). For a generator, each successful call returns an iterator-result object with the shape { value, done }, rather than another iterator.
done: false: the generator has yieldedvalueand is suspended.done: true: the generator has completed, andvaluecan contain its final return value.
Completion doesn't restart the generator: subsequent next() calls return { value: undefined, done: true }.
Passing values back into a generator
The important distinction is between the value sent out by yield and the value sent in by the following next(value). The first next() argument is ignored because no yield expression is suspended yet.
function* myGen() {
console.log("body started");
const a = yield "first yield";
console.log(a);
const b = yield "second yield";
console.log(b);
return "complete";
}
const generator = myGen(); // Body has not executed.
console.log(generator.next("ignored"));
// Logs "body started", then { value: "first yield", done: false }
console.log(generator.next("hello"));
// Logs "hello", then { value: "second yield", done: false }
console.log(generator.next("world"));
// Logs "world", then { value: "complete", done: true }
console.log(generator.next());
// { value: undefined, done: true }Here, a becomes "hello", not "first yield". The assignment finishes only when the caller resumes the suspended expression.
myGen() -> iterator created; body not started
next("ignored") -> yields "first yield"
next("hello") -> a = "hello"; yields "second yield"
next("world") -> b = "world"; returns "complete"Consuming Generators with for...of and Spread
A generator object is both an iterator and an iterable: an object that iteration syntax can consume. Its [Symbol.iterator]() method returns itself, so you can use it directly with for...of or spread.
function* numbers() {
yield 1;
yield 2;
return 99;
}
for (const number of numbers()) {
console.log(number); // Logs 1, then 2; never 99.
}
const sequence = numbers();
console.log([...sequence]); // [1, 2]
console.log([...sequence]); // []: the same iterator is exhausted.These consumers stop when done becomes true, without exposing that result's value. If you need a final return value such as 99, inspect the result of manual next() calls instead. See JavaScript iteration and loops for the surrounding loop syntax.
Using Generators in the Real World: Lazy Filtering
A useful generator computes results only as the consumer requests them. This is lazy evaluation: you defer work until its result is needed, which is useful for sequences you might stop consuming early.
function* matching(records, predicate) {
for (const record of records) {
if (predicate(record)) {
yield record;
}
}
}
const records = [
{ id: 1, status: "closed" },
{ id: 2, status: "open" },
{ id: 3, status: "open" },
];
for (const record of matching(records, (item) => item.status === "open")) {
console.log(record.id); // 2
break;
}In this example, the consumer stops after the first match, so the predicate never examines record 3. You avoid building an intermediate results array, although the original input array still exists.
Choose this pattern when incremental consumption matters. If you need every result immediately, an ordinary array transformation may be simpler; generator syntax isn't automatically a performance improvement.
Stopping, Throwing, and Cleaning Up Generators
Generators expose two control methods beyond next():
return(value)requests an early return at the suspended position. A surroundingfinallyblock can perform cleanup.throw(error)injects an exception at the suspended position. A surroundingcatchcan handle it and even yield another value; an uncaught error propagates to the caller.
function* work() {
try {
yield "ready";
} catch (error) {
yield `recovered: ${error.message}`;
} finally {
console.log("cleanup");
}
}
const stopped = work();
stopped.next();
console.log(stopped.return("stopped"));
// Logs "cleanup", then { value: "stopped", done: true }
const recovered = work();
recovered.next();
console.log(recovered.throw(new Error("retry")));
// { value: "recovered: retry", done: false }
console.log(recovered.next());
// Logs "cleanup", then { value: undefined, done: true }Breaking out of for...of calls the iterator's return() method when present. For a generator suspended inside a try block, that gives its finally block a chance to release resources.
One caveat matters: if finally itself yields, return() can produce done: false rather than finish immediately. Keep cleanup non-yielding unless your consumer explicitly supports that behavior. Also, calling return() before the first next() doesn't execute the body or enter its finally blocks.
Does yield Await a Promise?
The yield operator suspends execution and exposes a value. It doesn't attach Promise handlers or automatically advance the iterator when a Promise settles. This distinction matters whenever you combine generators with JavaScript Promises.
A complete Promise-aware runner
For historical or specialized generator-based asynchronous control flow, you need a runner: code that awaits yielded values, sends fulfillment values back through next(), and sends rejections through throw(). This example includes the missing orchestration.
function makeRequest() {
return new Promise((resolve) => {
setTimeout(() => resolve("success"), 500);
});
}
async function run(iterator) {
let result = iterator.next();
while (!result.done) {
let value;
try {
value = await result.value;
} catch (error) {
result = iterator.throw(error);
continue;
}
result = iterator.next(value);
}
return result.value;
}
function* requestFlow() {
const response = yield makeRequest();
return response;
}
run(requestFlow())
.then((value) => console.log(value)) // success
.catch((error) => console.error("Request failed:", error));Here, run() performs the wait; the generator merely yields and resumes. A generator's own try...catch can handle a rejection only because the runner forwards it through throw(). If it remains uncaught, the runner's returned Promise rejects.
This implementation awaits each yielded value; it doesn't recursively run nested generators, interpret arrays as parallel tasks, or provide cancellation. Treat it as an explanation of the protocol, not a new general-purpose application framework.
The usual modern choice: async/await
For ordinary sequential asynchronous work, use an async function. Async functions return Promises, and await provides built-in Promise-based suspension, including support for thenables—objects with a callable then method. You don't need a custom runner.
// Fragment: uses makeRequest() from the preceding example.
async function request() {
const response = await makeRequest();
return response;
}
request()
.then((value) => console.log(value)) // success
.catch((error) => console.error("Request failed:", error));For more examples of that approach, see JavaScript async/await examples.
Generators vs Async Functions vs Async Generators
| Construct | Calling it returns | Best fit |
|---|---|---|
function* | A synchronous generator iterator | Lazy sequences and caller-controlled iteration. |
async function | A Promise | One asynchronous result with built-in await. |
async function* | An async generator iterator whose next() returns a Promise | A sequence whose values require asynchronous work. |
Async generators and for await...of
An async generator combines await for waiting with yield for incremental delivery. Consume it with for await...of, which awaits successive iterator results.
This runnable pagination example uses a deterministic fixture instead of a remote service. Its page contract is { items, nextCursor }, with null marking the end:
async function fetchPage(cursor) {
return cursor === 0
? { items: [1, 2], nextCursor: 1 }
: { items: [3], nextCursor: null };
}
async function* paginate(fetchPage) {
let cursor = 0;
while (cursor !== null) {
const { items, nextCursor } = await fetchPage(cursor);
for (const item of items) {
yield item;
}
cursor = nextCursor;
}
}
async function main() {
for await (const item of paginate(fetchPage)) {
console.log(item); // Logs 1, then 2, then 3.
}
}
main().catch((error) => console.error("Pagination failed:", error));In this implementation, the next page isn't requested until the consumer advances beyond the current page's items. For a real API client, define page validation, cursor termination, request failures, and cancellation explicitly.
Unlike a synchronous generator, an async generator also unwraps yielded Promises: its resolved iterator result contains the fulfillment value. If a yielded Promise rejects, the rejection is thrown back into the generator at that yield, so a surrounding try...catch can handle it; only an uncaught rejection rejects the corresponding next() Promise.
Runtime support
MDN lists function* as widely available across browsers since September 2016, and async function* since January 2020. Check their compatibility tables against your actual target browsers, and test older embedded or server runtimes rather than assuming that an “ES6-compatible” label covers asynchronous iteration.
Common Mistakes
- Reusing an exhausted iterator. Call the generator function again to get a fresh iterator; a completed generator doesn't restart.
- Spreading an unbounded sequence. Spread collects the sequence's yielded values into an array. Only use it when you know the sequence is finite and suitable to materialize.
- Abandoning a generator instead of closing it. Merely dropping your reference doesn't execute a suspended
finallyblock. Explicitly finish or close iterators you drive manually. - Expecting yield to make CPU-heavy work nonblocking. A synchronous
next()call executes generator code until it yields, returns, or throws; generators don't automatically schedule that work asynchronously.
Use generators when consumer-controlled execution makes your design clearer. Use async/await for ordinary asynchronous operations, and async generators when you need both waiting and incremental delivery.
FAQ
What does a JavaScript generator function return?
A synchronous generator function returns a Generator object that is both an iterator and an iterable. Calling the function doesn't execute its body; the first next() call starts execution.
How do yield and next() work together?
yield exposes a value through the result of next(). A later next(value) makes value the result of the suspended yield expression; the argument to the first next() is ignored.
Does yield await a Promise?
Not in a synchronous generator: yield promise exposes the Promise object without waiting for it. A runner can await it and resume the generator; async generators have different, Promise-aware yield semantics.
When should I use a generator instead of async/await?
Use a generator for lazy sequences, custom iteration, or a protocol where the caller explicitly advances execution. Use async/await when you want one Promise-based result from asynchronous work.
What is the difference between a generator and an async generator?
A synchronous generator's next() returns an iterator-result object immediately. An async generator's next() returns a Promise for an iterator-result object, and its body can use both await and yield; consume it with for await...of.
Further Reading
- MDN: Iterators and generators — practical iteration patterns and lazy sequences.
- MDN: Generator.prototype.next() — result objects and values sent into a generator.
- MDN: Generator.prototype.return() — early completion and finally behavior.
- MDN: Generator.prototype.throw() — injected exceptions and recovery.
- MDN: Async functions — native Promise-based control flow.
- MDN: Async generator functions — asynchronous iteration and yielded Promises.
- MDN: for await...of — consuming async iterables and early-exit cleanup.
- ECMAScript specification: Generator objects — the normative generator protocol and execution states.
