How to Use Generators in JavaScript
Regular functions in JavaScript follow a simple rule: they run from start to finish without stopping. Once called, they execute every line and return a single value. Generators break this rule entirely. A generator function can pause its execution at any point, return an intermediate value, and later resume exactly where it left off, remembering its entire internal state.
This ability to pause and resume makes generators uniquely powerful. They can produce sequences of values on demand (lazy evaluation), create infinite data streams without consuming infinite memory, manage complex stateful logic, and serve as the foundation for advanced patterns like async iteration.
This guide walks you through generator syntax, the protocol they use to communicate, how to compose them, and the real-world problems they solve.
Generator Functions: function* Syntax
A generator function is declared with an asterisk (*) after the function keyword. Inside the function body, the yield keyword marks pause points where the function produces a value and suspends.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
The asterisk can be placed in several positions. All of these are valid and equivalent:
function* gen() {} // Most common style
function *gen() {} // Also valid
function * gen() {} // Also valid
Calling a Generator Function Does Not Execute It
This is the first surprise. Calling a generator function does not run its body. Instead, it returns a special generator object that controls the execution:
function* greet() {
console.log("Hello!");
yield 1;
console.log("World!");
yield 2;
}
const gen = greet(); // Nothing is logged! The body hasn't run yet.
console.log(gen); // Object [Generator] {}
No output appears. The function body is completely frozen, waiting for instructions from the generator object. Think of calling a generator function as loading a program into a paused state. You need to explicitly tell it to run.
Generator Methods and Expressions
Generators can be defined as methods inside objects and classes:
const obj = {
*generate() {
yield "a";
yield "b";
}
};
class DataSource {
*items() {
yield "item1";
yield "item2";
}
}
const source = new DataSource();
const gen = source.items();
console.log(gen.next()); // { value: "item1", done: false }
Generator expressions (anonymous generators) also work:
const gen = function*() {
yield 1;
yield 2;
};
const iterator = gen();
console.log(iterator.next()); // { value: 1, done: false }
Arrow functions cannot be generators. There is no *=> syntax. This is by design, as generators rely on their own this binding and the ability to be paused, which conflicts with the lightweight nature of arrow functions.
The Generator Object: next(), { value, done }
The generator object returned by a generator function implements the iterator protocol. Its primary method is next(), which resumes the generator's execution until the next yield or return.
How next() Works
Each call to next() returns an object with two properties:
value: the value produced byyieldorreturndone:falseif the generator can produce more values,trueif it has finished
function* count() {
yield "one";
yield "two";
yield "three";
}
const gen = count();
console.log(gen.next()); // { value: "one", done: false }
console.log(gen.next()); // { value: "two", done: false }
console.log(gen.next()); // { value: "three", done: false }
console.log(gen.next()); // { value: undefined, done: true }
console.log(gen.next()); // { value: undefined, done: true } (stays done)
Step-by-Step Execution
Let us trace through a generator with side effects to see exactly when each line executes:
function* detailed() {
console.log("A: before first yield");
yield 1;
console.log("B: after first yield, before second");
yield 2;
console.log("C: after second yield, before return");
return 3;
console.log("D: after return"); // Never reached
yield 4; // Never reached
}
const gen = detailed();
console.log("--- Calling first next() ---");
console.log(gen.next());
// A: before first yield
// { value: 1, done: false }
console.log("--- Calling second next() ---");
console.log(gen.next());
// B: after first yield, before second
// { value: 2, done: false }
console.log("--- Calling third next() ---");
console.log(gen.next());
// C: after second yield, before return
// { value: 3, done: true } ← note: done is TRUE for return
console.log("--- Calling fourth next() ---");
console.log(gen.next());
// { value: undefined, done: true }
Notice that return produces done: true, while yield produces done: false. Code after a return statement never executes.
The Difference Between yield and return
Both produce values, but they behave differently:
function* withReturn() {
yield 1;
yield 2;
return 3; // Final value, done: true
}
function* withoutReturn() {
yield 1;
yield 2;
// Implicit return undefined
}
// With explicit return
const gen1 = withReturn();
console.log(gen1.next()); // { value: 1, done: false }
console.log(gen1.next()); // { value: 2, done: false }
console.log(gen1.next()); // { value: 3, done: true }
// Without return
const gen2 = withoutReturn();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.next()); // { value: 2, done: false }
console.log(gen2.next()); // { value: undefined, done: true }
When using generators with for...of, the return value is ignored. Only yield values are iterated. This is because for...of stops when it sees done: true, discarding the accompanying value:
function* gen() {
yield 1;
yield 2;
return 3; // This value is lost in for...of
}
for (const val of gen()) {
console.log(val);
}
// 1
// 2
// (3 is NOT logged)
Generators Are Iterable
Generator objects implement the iterable protocol (they have a Symbol.iterator method that returns themselves). This means they work with all iteration constructs in JavaScript.
for...of Loops
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take first 10 Fibonacci numbers
let count = 0;
for (const num of fibonacci()) {
console.log(num);
if (++count >= 10) break;
}
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Spread Syntax
function* vowels() {
yield "a";
yield "e";
yield "i";
yield "o";
yield "u";
}
const vowelArray = [...vowels()];
console.log(vowelArray); // ["a", "e", "i", "o", "u"]
Destructuring
function* coordinates() {
yield 10;
yield 20;
yield 30;
}
const [x, y, z] = coordinates();
console.log(x, y, z); // 10 20 30
// Partial destructuring
const [first, ...rest] = (function*() {
yield 1;
yield 2;
yield 3;
yield 4;
})();
console.log(first); // 1
console.log(rest); // [2, 3, 4]
Array.from()
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
const arr = Array.from(range(0, 10, 2));
console.log(arr); // [0, 2, 4, 6, 8]
Making Objects Iterable with Generators
Generators provide the simplest way to make custom objects iterable:
const playlist = {
tracks: ["Song A", "Song B", "Song C", "Song D"],
*[Symbol.iterator]() {
for (const track of this.tracks) {
yield track;
}
}
};
for (const track of playlist) {
console.log(track);
}
// Song A
// Song B
// Song C
// Song D
console.log([...playlist]); // ["Song A", "Song B", "Song C", "Song D"]
Compare this with the manual iterator approach:
// Without generators (verbose)
const playlist = {
tracks: ["Song A", "Song B", "Song C"],
[Symbol.iterator]() {
let index = 0;
const tracks = this.tracks;
return {
next() {
if (index < tracks.length) {
return { value: tracks[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
};
// With generators (concise and clear)
const playlistGen = {
tracks: ["Song A", "Song B", "Song C"],
*[Symbol.iterator]() {
yield* this.tracks;
}
};
The generator version is dramatically simpler because the generator protocol handles all the state management (index tracking, building { value, done } objects) automatically.
yield: Pausing and Resuming Execution
The yield keyword is the heart of generators. It does two things simultaneously:
- Sends a value out to the caller (as
next()'s return value) - Pauses the generator's execution, freezing all local variables and the current position
When next() is called again, execution resumes from exactly where it paused, with all local state intact.
Local State Is Preserved
function* stateful() {
let count = 0;
while (true) {
count++;
console.log(`Before yield: count = ${count}`);
yield count;
console.log(`After yield: count = ${count}`);
}
}
const gen = stateful();
console.log(gen.next());
// Before yield: count = 1
// { value: 1, done: false }
// count is still 1 when we resume
console.log(gen.next());
// After yield: count = 1
// Before yield: count = 2
// { value: 2, done: false }
console.log(gen.next());
// After yield: count = 2
// Before yield: count = 3
// { value: 3, done: false }
The count variable survives across yield pauses. The generator remembers everything: local variables, loop positions, conditional states, the program counter.
yield in Expressions
yield can appear inside expressions. The value of the yield expression itself is determined by what gets passed into the next next() call (covered in the next section). But the value sent out is whatever follows yield:
function* computed() {
const a = 10;
const b = 20;
yield a + b; // Yields 30
yield a * b; // Yields 200
yield `${a} + ${b} = ${a + b}`; // Yields "10 + 20 = 30"
}
const gen = computed();
console.log(gen.next().value); // 30
console.log(gen.next().value); // 200
console.log(gen.next().value); // "10 + 20 = 30"
Conditional and Loop Yields
Generators can use any control flow around yield:
function* conditionalGen(type) {
if (type === "numbers") {
yield 1;
yield 2;
yield 3;
} else if (type === "letters") {
yield "a";
yield "b";
yield "c";
} else {
yield "unknown type";
}
}
console.log([...conditionalGen("numbers")]); // [1, 2, 3]
console.log([...conditionalGen("letters")]); // ["a", "b", "c"]
console.log([...conditionalGen("other")]); // ["unknown type"]
function* powers(base, limit) {
let exponent = 0;
let result = 1;
while (result <= limit) {
yield result;
exponent++;
result = base ** exponent;
}
}
console.log([...powers(2, 1000)]);
// [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
console.log([...powers(3, 100)]);
// [1, 3, 9, 27, 81]
Passing Values into Generators via next(value)
Communication with generators is two-way. So far, we have only sent values out of the generator (via yield). But you can also send values into the generator by passing an argument to next().
The value passed to next(value) becomes the result of the yield expression that is currently paused.
Basic Two-Way Communication
function* conversation() {
const name = yield "What is your name?";
const age = yield `Hello, ${name}! How old are you?`;
return `${name} is ${age} years old.`;
}
const gen = conversation();
// First next() starts the generator (any argument is ignored)
const q1 = gen.next();
console.log(q1.value); // "What is your name?"
// Pass the answer to the first question
const q2 = gen.next("Alice");
console.log(q2.value); // "Hello, Alice! How old are you?"
// Pass the answer to the second question
const result = gen.next(30);
console.log(result.value); // "Alice is 30 years old."
console.log(result.done); // true
Understanding the Flow
This is the trickiest part of generators. Let us trace through the execution step by step:
function* twoWay() {
console.log("Generator started");
const a = yield "first yield";
console.log("Received a:", a);
const b = yield "second yield";
console.log("Received b:", b);
return a + b;
}
const gen = twoWay();
Step 1: gen.next() (first call, argument is ignored)
const r1 = gen.next("this is ignored");
// Generator started ← body runs until first yield
// r1 = { value: "first yield", done: false }
The generator runs from the beginning until it hits yield "first yield". It pauses at the yield, before assigning the yield's result to a. Any argument to the first next() is discarded because there is no yield waiting to receive it.
Step 2: gen.next(10)
const r2 = gen.next(10);
// Received a: 10 ← 10 becomes the result of the first yield
// r2 = { value: "second yield", done: false }
The value 10 is injected as the result of yield "first yield", so a = 10. Execution continues until the next yield.
Step 3: gen.next(20)
const r3 = gen.next(20);
// Received b: 20 ← 20 becomes the result of the second yield
// r3 = { value: 30, done: true } ← return a + b = 10 + 20
The value 20 is injected as the result of yield "second yield", so b = 20. The function returns a + b = 30.
A Practical Example: Accumulator
function* runningTotal() {
let total = 0;
while (true) {
const value = yield total;
if (value === null) return total; // Sentinel value to stop
total += value;
}
}
const acc = runningTotal();
acc.next(); // Start the generator, { value: 0, done: false }
console.log(acc.next(10).value); // 10
console.log(acc.next(20).value); // 30
console.log(acc.next(5).value); // 35
console.log(acc.next(-15).value); // 20
console.log(acc.next(null)); // { value: 20, done: true }
The first next() call starts the generator. Any value passed to it is ignored because no yield is waiting to receive a value. This catches many people off guard. Always call the first next() without arguments (or accept that the argument is discarded).
generator.throw(error): Injecting Errors
The throw() method injects an error into the generator at the point where it is currently paused. The error appears as if the yield expression itself threw it. If the generator has a try...catch around the yield, it can handle the error and continue.
function* resilient() {
let result;
while (true) {
try {
result = yield result;
console.log("Received:", result);
} catch (e) {
console.log("Error caught inside generator:", e.message);
result = "recovered";
}
}
}
const gen = resilient();
gen.next(); // Start generator
console.log(gen.next(10));
// Received: 10
// { value: 10, done: false }
// Inject an error
console.log(gen.throw(new Error("something broke")));
// Error caught inside generator: something broke
// { value: "recovered", done: false }
// Generator continues normally after catching the error
console.log(gen.next(20));
// Received: 20
// { value: 20, done: false }
Unhandled Errors Propagate Out
If the generator does not catch the thrown error, it propagates out to the caller:
function* fragile() {
yield 1;
yield 2; // No try...catch
yield 3;
}
const gen = fragile();
gen.next(); // { value: 1, done: false }
try {
gen.throw(new Error("crash"));
} catch (e) {
console.log("Caught outside:", e.message); // "crash"
}
// Generator is now done: it was terminated by the unhandled error
console.log(gen.next()); // { value: undefined, done: true }
Error Injection for Testing
function* fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = yield `Fetching ${url} (attempt ${attempt})`;
return response;
} catch (e) {
console.log(`Attempt ${attempt} failed: ${e.message}`);
if (attempt === maxRetries) {
throw new Error(`All ${maxRetries} attempts failed for ${url}`);
}
}
}
}
const gen = fetchWithRetry("https://api.example.com/data");
console.log(gen.next().value);
// "Fetching https://api.example.com/data (attempt 1)"
// Simulate first failure
console.log(gen.throw(new Error("Network timeout")).value);
// Attempt 1 failed: Network timeout
// "Fetching https://api.example.com/data (attempt 2)"
// Simulate second failure
console.log(gen.throw(new Error("Server error")).value);
// Attempt 2 failed: Server error
// "Fetching https://api.example.com/data (attempt 3)"
// Simulate success on third attempt
console.log(gen.next({ data: "success" }));
// { value: { data: "success" }, done: true }
generator.return(value): Completing Early
The return() method forces the generator to finish, as if a return statement was executed at the current pause point. It sets the generator's state to done and returns { value, done: true }.
function* counting() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const gen = counting();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.return("end")); // { value: "end", done: true }
console.log(gen.next()); // { value: undefined, done: true } (permanently done)
return() and finally
If the generator has a try...finally block around the current yield, the finally block still runs when return() is called:
function* withCleanup() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("Cleanup! Finally block runs.");
yield "from finally"; // yield inside finally works!
}
}
const gen = withCleanup();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return("early"));
// Cleanup! Finally block runs.
// { value: "from finally", done: false } ← yield in finally delays completion!
console.log(gen.next()); // { value: "early", done: true } ← now truly done
This is important for generators that manage resources (file handles, database connections, event listeners). The finally block ensures cleanup happens even if the consumer stops iterating early.
for...of and Early Termination
When a for...of loop breaks out of a generator early, it automatically calls return() on the generator:
function* loggingGenerator() {
try {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
} finally {
console.log("Generator was closed (return() called by for...of)");
}
}
for (const val of loggingGenerator()) {
console.log(val);
if (val === 3) break; // This triggers gen.return()
}
// 1
// 2
// 3
// Generator was closed (return() called by for...of)
Generator Composition: yield*
The yield* expression delegates to another iterable or generator. Instead of yielding the iterable itself as a single value, it yields each of its values one by one, as if they were yielded directly by the outer generator.
Basic Delegation
function* inner() {
yield "a";
yield "b";
yield "c";
}
function* outer() {
yield 1;
yield* inner(); // Delegate to inner generator
yield 2;
}
console.log([...outer()]); // [1, "a", "b", "c", 2]
Without yield*, you would get a nested generator object instead:
function* outerWrong() {
yield 1;
yield inner(); // Yields the generator OBJECT, not its values
yield 2;
}
console.log([...outerWrong()]); // [1, Object [Generator] {}, 2] (wrong!)
yield* Works with Any Iterable
Arrays, strings, Maps, Sets, and any other iterable can be delegated to:
function* mixed() {
yield* [1, 2, 3]; // Array
yield* "hello"; // String (yields individual characters)
yield* new Set([4, 5]); // Set
}
console.log([...mixed()]); // [1, 2, 3, "h", "e", "l", "l", "o", 4, 5]
Recursive Generator Composition
yield* enables elegant recursive patterns, especially for tree traversal:
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // Recurse into nested arrays
} else {
yield item;
}
}
}
const nested = [1, [2, [3, 4], 5], [6, [7, [8]]], 9];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Tree Traversal
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// In-order traversal using generator composition
*[Symbol.iterator]() {
yield this.value;
for (const child of this.children) {
yield* child; // Recursively delegate to child nodes
}
}
}
const tree = new TreeNode("root", [
new TreeNode("A", [
new TreeNode("A1"),
new TreeNode("A2")
]),
new TreeNode("B", [
new TreeNode("B1", [
new TreeNode("B1a"),
new TreeNode("B1b")
])
]),
new TreeNode("C")
]);
console.log([...tree]);
// ["root", "A", "A1", "A2", "B", "B1", "B1a", "B1b", "C"]
for (const node of tree) {
console.log(node);
}
// root, A, A1, A2, B, B1, B1a, B1b, C
The Return Value of yield*
yield* evaluates to the return value of the delegated generator (the value when done is true):
function* inner() {
yield "x";
yield "y";
return "inner's return value"; // This becomes the result of yield*
}
function* outer() {
const result = yield* inner();
console.log("inner returned:", result);
yield "z";
}
console.log([...outer()]);
// inner returned: inner's return value
// ["x", "y", "z"]
The return value of inner() ("inner's return value") does not appear in the iteration output (because done: true values are skipped), but it is captured as the result of the yield* expression.
Composing Multiple Generators
function* header() {
yield "=== REPORT ===";
yield `Date: ${new Date().toLocaleDateString()}`;
yield "";
}
function* body(items) {
for (const [i, item] of items.entries()) {
yield `${i + 1}. ${item}`;
}
yield "";
}
function* footer(total) {
yield `Total items: ${total}`;
yield "=== END ===";
}
function* report(items) {
yield* header();
yield* body(items);
yield* footer(items.length);
}
const items = ["Widget", "Gadget", "Doohickey"];
for (const line of report(items)) {
console.log(line);
}
// === REPORT ===
// Date: 1/15/2024
//
// 1. Widget
// 2. Gadget
// 3. Doohickey
//
// Total items: 3
// === END ===
Use Cases: Lazy Sequences, Unique ID Generators, State Machines
Generators are not just an academic curiosity. They solve real problems elegantly.
Lazy Sequences
Generators produce values on demand. They compute the next value only when asked, which means you can represent sequences of any size, including infinite ones, without allocating memory for all values upfront.
// Infinite sequence: only computes what you consume
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++;
}
}
// Take only what you need
function take(n, iterable) {
const result = [];
for (const item of iterable) {
result.push(item);
if (result.length >= n) break;
}
return result;
}
console.log(take(5, naturalNumbers())); // [1, 2, 3, 4, 5]
You can chain lazy transformations:
function* map(fn, iterable) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(predicate, iterable) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* takeWhile(predicate, iterable) {
for (const item of iterable) {
if (!predicate(item)) return;
yield item;
}
}
// Compose: natural numbers → keep evens → square them → take while < 1000
const result = [
...takeWhile(
x => x < 1000,
map(
x => x * x,
filter(
x => x % 2 === 0,
naturalNumbers()
)
)
)
];
console.log(result);
// [4, 16, 36, 64, 100, 144, 196, 256, 324, 400, 484, 576, 676, 784, 900]
No intermediate arrays are created. Each value flows through the entire pipeline one at a time.
Unique ID Generator
function* idGenerator(prefix = "id") {
let id = 0;
while (true) {
yield `${prefix}_${(++id).toString(36).padStart(6, "0")}`;
}
}
const userIds = idGenerator("user");
const sessionIds = idGenerator("sess");
console.log(userIds.next().value); // "user_000001"
console.log(userIds.next().value); // "user_000002"
console.log(sessionIds.next().value); // "sess_000001"
console.log(userIds.next().value); // "user_000003"
console.log(sessionIds.next().value); // "sess_000002"
Each generator maintains its own independent counter.
Range Generator
function* range(start, end, step = 1) {
if (step > 0) {
for (let i = start; i < end; i += step) {
yield i;
}
} else if (step < 0) {
for (let i = start; i > end; i += step) {
yield i;
}
}
}
console.log([...range(0, 10)]); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8]
console.log([...range(10, 0, -3)]); // [10, 7, 4, 1]
console.log([...range(0, 1, 0.2)]); // [0, 0.2, 0.4, 0.6, 0.8] (floating point)
Paginated Data Source
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield {
page: Math.floor(i / pageSize) + 1,
data: items.slice(i, i + pageSize),
hasMore: i + pageSize < items.length
};
}
}
const allItems = Array.from({ length: 23 }, (_, i) => `Item ${i + 1}`);
const pages = paginate(allItems, 5);
console.log(pages.next().value);
// { page: 1, data: ["Item 1", ... "Item 5"], hasMore: true }
console.log(pages.next().value);
// { page: 2, data: ["Item 6", ... "Item 10"], hasMore: true }
// Skip to spread all remaining
for (const page of pages) {
console.log(`Page ${page.page}: ${page.data.length} items, hasMore: ${page.hasMore}`);
}
// Page 3: 5 items, hasMore: true
// Page 4: 5 items, hasMore: true
// Page 5: 3 items, hasMore: false
State Machine
Generators naturally model state machines because they maintain internal state and transition between states at each yield:
function* trafficLight() {
while (true) {
yield "green";
yield "yellow";
yield "red";
}
}
const light = trafficLight();
console.log(light.next().value); // "green"
console.log(light.next().value); // "yellow"
console.log(light.next().value); // "red"
console.log(light.next().value); // "green" (cycles back)
A more complex state machine with conditional transitions:
function* orderStateMachine() {
console.log("Order created");
const paymentResult = yield "pending_payment";
if (paymentResult === "paid") {
console.log("Payment received");
const fulfillResult = yield "processing";
if (fulfillResult === "shipped") {
console.log("Order shipped");
const deliveryResult = yield "shipped";
if (deliveryResult === "delivered") {
return "completed";
} else {
return "delivery_failed";
}
} else if (fulfillResult === "out_of_stock") {
console.log("Refunding...");
return "refunded";
}
} else if (paymentResult === "declined") {
console.log("Payment declined");
return "cancelled";
}
}
// Simulate a successful order
const order = orderStateMachine();
console.log(order.next().value); // "pending_payment"
console.log(order.next("paid").value); // "processing"
console.log(order.next("shipped").value); // "shipped"
console.log(order.next("delivered").value); // "completed"
Cooperative Task Scheduling
function* task(name, steps) {
for (let i = 1; i <= steps; i++) {
console.log(`${name}: step ${i}/${steps}`);
yield; // Yield control back to scheduler
}
console.log(`${name}: completed`);
}
function runTasks(tasks) {
const generators = tasks.map(t => t());
while (generators.length > 0) {
for (let i = generators.length - 1; i >= 0; i--) {
const result = generators[i].next();
if (result.done) {
generators.splice(i, 1);
}
}
}
}
runTasks([
() => task("Download", 3),
() => task("Parse", 2),
() => task("Render", 4)
]);
Output:
Render: step 1/4
Parse: step 1/2
Download: step 1/3
Render: step 2/4
Parse: step 2/2
Download: step 2/3
Render: step 3/4
Parse: completed
Download: step 3/3
Render: step 4/4
Download: completed
Render: completed
The tasks interleave their execution cooperatively, each yielding control after one step.
Summary
Generators introduce a fundamentally different execution model to JavaScript. They transform functions from run-to-completion units into pausable, resumable, two-way communication channels.
| Concept | Key Point |
|---|---|
function* | Declares a generator function. Calling it returns a generator object, does not execute the body. |
yield | Pauses execution and sends a value out. Resumes on the next next() call. |
next() | Resumes the generator. Returns { value, done }. |
next(value) | Resumes and injects value as the result of the paused yield. First next() argument is ignored. |
throw(error) | Injects an error at the paused yield. Generator can catch it with try...catch. |
return(value) | Forces the generator to finish. finally blocks still run. |
yield* | Delegates to another iterable or generator, yielding each of its values. |
| Iterability | Generators work with for...of, spread [...], destructuring, and Array.from(). |
return vs yield | return sets done: true and the value is skipped by for...of. yield sets done: false. |
| Lazy evaluation | Values are computed only when requested. Infinite sequences use finite memory. |
| State preservation | Local variables, loop counters, and execution position survive across yield pauses. |
Key rules to remember:
- The first
next()call starts the generator; any argument passed to it is discarded yieldboth sends a value out and receives a value in (on the nextnext()call)for...ofignores thereturnvalue of a generator (it stops atdone: true)for...ofautomatically callsreturn()on early termination (break), triggeringfinallyblocksyield*flattens delegation, yielding each value individually rather than yielding the iterable as a single value- Generators are single-use: once exhausted, calling
next()always returns{ value: undefined, done: true }