Technology · JavaScript
AdvancedJavaScript Generators
Learn how generator functions pause and resume execution to build lazy sequences and iterables.
TL;DR
- 01Declare generators with function* to pause and resume execution.
- 02Use yield to emit values one at a time, lazily.
- 03Delegate to nested generators or iterables with yield*.
Generator Basics
- Use
function*to declare a generator that can pause and resume execution withyield.function* counter() { yield 1; yield 2; } const it = counter(); it.next(); // { value: 1, done: false } - Calling a generator function does not run its body — it returns an iterator instead.
function* greet() { console.log("started"); yield "hi"; } const it = greet(); // nothing logged yet it.next(); // logs "started", returns { value: "hi", done: false } - Each call to
.next()runs the body until the nextyieldor the end of the function.function* counter() { yield 1; yield 2; } const it = counter(); [...it]; // [1, 2] - The final
.next()call returnsdone: truewithvalueset to the return value, if any.function* range() { yield 1; return "finished"; } const it = range(); it.next(); // { value: 1, done: false } it.next(); // { value: "finished", done: true } - Generators implement both the iterable and iterator protocols, so
for...ofworks directly on them.function* counter() { yield 1; yield 2; } for (const n of counter()) console.log(n); // 1, 2
Controlling Generators with next, return, and throw
- Pass a value to
next(value)to inject data back into the generator at the pausedyield.function* greet() { const name = yield "name?"; yield `Hello, ${name}!`; } const g = greet(); g.next(); // { value: "name?", done: false } g.next("Ada"); // { value: "Hello, Ada!", done: false } - Call
.return(value)to stop the generator early, as if areturnran at the currentyield.function* counter() { yield 1; yield 2; } const it = counter(); it.next(); // { value: 1, done: false } it.return("stop"); // { value: "stop", done: true } - Call
.throw(error)to inject an exception at the pausedyield, which the generator can catch.function* safeGen() { try { yield 1; } catch (e) { yield `caught: ${e.message}`; } } const it = safeGen(); it.next(); it.throw(new Error("oops")); // { value: "caught: oops", done: false } - An uncaught
throw()closes the generator permanently, so laternext()calls just returndone: true.function* gen() { yield 1; } const it = gen(); it.next(); try { it.throw(new Error("fatal")); } catch (e) { console.log(e.message); // "fatal" } it.next(); // { value: undefined, done: true } try/finallyblocks inside a generator still run cleanup code when.return()is called.function* withCleanup() { try { yield 1; yield 2; } finally { console.log("cleanup ran"); } } const it = withCleanup(); it.next(); it.return(); // logs "cleanup ran"
Lazy Sequences
- Generators compute values on demand, so they can represent infinite sequences without infinite memory.
function* naturals() { let n = 1; while (true) yield n++; } const it = naturals(); it.next().value; // 1 it.next().value; // 2 - Combine a generator with
take-style logic to pull only the values you need.function take(iterable, count) { const result = []; for (const value of iterable) { if (result.length >= count) break; result.push(value); } return result; } take(naturals(), 3); // [1, 2, 3] - Lazy evaluation avoids wasted work, since nothing computes until something consumes the value.
function* expensiveValues() { yield computeA(); // only runs when first value is requested yield computeB(); } - Chain generators together to build lazy pipelines, similar to map and filter but evaluated one item at a time.
function* mapGen(iterable, fn) { for (const value of iterable) yield fn(value); } function* filterGen(iterable, predicate) { for (const value of iterable) if (predicate(value)) yield value; } const evens = filterGen(naturals(), n => n % 2 === 0); take(evens, 3); // [2, 4, 6] - Avoid spreading an infinite generator directly into an array, since that runs forever and crashes the process.
// Never do this: // const all = [...naturals()];
Building Custom Iterables
- Assign a generator to
Symbol.iteratorto make any object work withfor...ofand spread syntax.class Range { constructor(start, end) { this.start = start; this.end = end; } *[Symbol.iterator]() { for (let i = this.start; i <= this.end; i++) yield i; } } [...new Range(1, 3)]; // [1, 2, 3] - This removes the need to hand-write a
next()method with manualvalue/donebookkeeping.// Manual version requires tracking state yourself: // { next() { ... return { value, done }; } } // A generator method handles this automatically. - Generator methods work on plain objects too, not just classes.
const collection = { items: ["a", "b", "c"], *[Symbol.iterator]() { yield* this.items; } }; for (const item of collection) console.log(item); - Combine multiple sources into one iterable by yielding from each in sequence.
function* combined(...iterables) { for (const iterable of iterables) yield* iterable; } [...combined([1, 2], [3, 4])]; // [1, 2, 3, 4] - Custom iterables built with generators work everywhere standard iterables do, including destructuring.
const [first, second] = new Range(10, 20); // first === 10, second === 11
Delegating with yield*
- Use
yield*to forward every value from another iterable without writing a manual loop.function* inner() { yield 1; yield 2; } function* outer() { yield* inner(); yield 3; } [...outer()]; // [1, 2, 3] yield*works on any iterable, not just other generators, including arrays and strings.function* flatten() { yield* [1, 2]; yield* "ab"; } [...flatten()]; // [1, 2, "a", "b"]yield*also forwardsnext()values andreturn()/throw()calls down to the delegated generator.function* inner() { const x = yield "ask"; yield `got ${x}`; } function* outer() { yield* inner(); } const it = outer(); it.next(); // { value: "ask", done: false } it.next("value"); // { value: "got value", done: false }- The expression
yield* iterableevaluates to the delegated generator's return value.function* inner() { yield 1; return "done"; } function* outer() { const result = yield* inner(); yield result; } [...outer()]; // [1, "done"] - Use recursive
yield*calls to flatten nested tree structures into a single sequence.function* flattenTree(node) { yield node.value; for (const child of node.children || []) { yield* flattenTree(child); } }
Tips
- 01Use generators to model lazy sequences, since values compute only when something calls next() on the iterator.
- 02Reach for yield* when delegating to another generator, so you avoid manually looping and re-yielding each value.
- 03Wrap a generator in a class method to give custom objects a clean Symbol.iterator implementation for free.
Warnings
- 01Calling next() on an exhausted generator just returns done: true forever, so create a fresh instance to iterate again.
- 02An uncaught error inside a generator body permanently closes it, so any later next() call just returns done.
- 03Storing a generator's full output in an array defeats its lazy nature and can exhaust memory on infinite sequences.
FAQ
- A regular function runs to completion the moment you call it. A generator function, declared with function*, returns an iterator immediately without running any code. Each call to next() resumes the function body until the next yield or until it returns.
- yield pauses the generator and sends a value out to whoever called next(). The generator stays frozen at that line until next() is called again. Whatever value you pass to that next() call becomes the result of the yield expression itself.
- yield* delegates iteration to another iterable, forwarding each of its values one by one. It saves you from writing a manual loop with repeated yield calls inside your generator. It also forwards next(), return(), and throw() calls down to the delegated iterable.
- next(value) resumes execution and injects a value at the paused yield. return(value) forces the generator to stop immediately, as if a return statement ran at that point. throw(error) injects an exception at the paused yield, which the generator can catch with a try/catch or let propagate.
- Yes, in most cases. Assign a generator function to Symbol.iterator on your object and the runtime handles the next() contract automatically. This is far less code than hand-writing an iterator object with its own value and done bookkeeping.