|$ curl https://forge-ai.dev/api/markdown?path=docs/js/generators
$cat docs/javascript-—-generators-&-iteration.md
updated Recently·30 min read·published

JavaScript — Generators & Iteration

JavaScriptGeneratorsAdvancedAdvanced
Introduction

Generators are a special class of functions in JavaScript that can pause execution, yield values, and resume later. Unlike regular functions that run-to-completion, generator functions produce a sequence of values on demand — they are lazy by nature. This enables powerful patterns like infinite sequences, cooperative multitasking, and custom iteration.

A generator function is defined with function* syntax and uses yield to produce values. When called, it does not execute immediately — instead it returns a Generator object that conforms to both the iterable and iterator protocols. Each call to .next() executes the function up to the next yield and then pauses.

Generators bridge the gap between synchronous and asynchronous programming. With async generators, you can produce promises lazily and consume them with for await...of. They are foundational to how libraries like Redux-Saga handle side effects and how modern JavaScript manages streams and infinite data.

generator-intro.js
JavaScript
1// A simple generator function
2function* counter() {
3 yield 1;
4 yield 2;
5 yield 3;
6}
7
8const gen = counter();
9console.log(gen.next()); // { value: 1, done: false }
10console.log(gen.next()); // { value: 2, done: false }
11console.log(gen.next()); // { value: 3, done: false }
12console.log(gen.next()); // { value: undefined, done: true }
13
14// Generators are iterable
15for (const value of counter()) {
16 console.log(value); // 1, 2, 3
17}
18
19// Spread works on generators
20const all = [...counter()]; // [1, 2, 3]
function* Syntax & yield

The function* declaration creates a generator function. The placement of the asterisk is flexible — function* name(), function *name(), and function * name() all work. The yield keyword appears only inside generator functions and pauses execution, sending a value to the consumer.

Generator Declaration Forms

FormSyntaxUsage
Declarationfunction* gen() Named generator function
Expressionconst gen = function*() Anonymous generator expression
Methodconst obj = { *gen() {} }Generator as object method
Classclass Foo { *gen() {} }Generator on class prototype
ArrowN/AArrow functions cannot be generators
syntax-forms.js
JavaScript
1// Various declaration forms
2function* countUp(limit) {
3 for (let i = 1; i <= limit; i++) {
4 yield i;
5 }
6}
7
8// Generator expression
9const countDown = function*(start) {
10 for (let i = start; i > 0; i--) {
11 yield i;
12 }
13};
14
15// Generator as object method
16const math = {
17 *fibonacci(n) {
18 let a = 0, b = 1;
19 for (let i = 0; i < n; i++) {
20 yield a;
21 [a, b] = [b, a + b];
22 }
23 }
24};
25
26// Generator in a class
27class Sequence {
28 constructor(start = 0) {
29 this.start = start;
30 }
31 *[Symbol.iterator]() {
32 let i = this.start;
33 while (true) yield i++;
34 }
35}
36
37// yield can be used in loops, conditionals, and expressions
38function* conditionalYield() {
39 yield 1;
40 if (Math.random() > 0.5) {
41 yield 2;
42 }
43 yield 3;
44}

info

Arrow functions cannot be generator functions. If you need a generator, use a regular function expression or a method shorthand. The asterisk (*) is part of the function declaration, not the name — it always comes after function (or after the method name in shorthand syntax).
Generator Execution Model (Lazy)

The defining characteristic of generators is lazy evaluation. The generator body does not execute when the function is called — it only executes piece by piece as .next() is invoked. Between yield points, the generator's execution context (local variables, scope, stack) is frozen.

This is fundamentally different from regular functions. A regular function runs entirely from start to finish and cannot be paused. A generator yields control back to the caller at each yield and the caller decides when to resume by calling .next() again.

lazy-execution.js
JavaScript
1// Demonstrating lazy execution
2function* lazySequence() {
3 console.log("Generator started");
4 yield 1;
5 console.log("Resumed after yield 1");
6 yield 2;
7 console.log("Resumed after yield 2");
8 yield 3;
9 console.log("Generator finished");
10}
11
12console.log("Before calling generator");
13const gen = lazySequence();
14console.log("Generator created, nothing executed yet");
15
16console.log(gen.next());
17// "Generator started"
18// { value: 1, done: false }
19
20console.log(gen.next());
21// "Resumed after yield 1"
22// { value: 2, done: false }
23
24console.log(gen.next());
25// "Resumed after yield 2"
26// { value: 3, done: false }
27
28console.log(gen.next());
29// "Generator finished"
30// { value: undefined, done: true }
31
32// Important: if you never call .next(), nothing executes
33// The generator is truly lazy — no computation until requested
lazy-vs-eager.js
JavaScript
1// Lazy evaluation vs eager
2function eagerRange(start, end) {
3 const result = [];
4 for (let i = start; i <= end; i++) {
5 result.push(i);
6 }
7 return result; // All computed upfront
8}
9
10function* lazyRange(start, end) {
11 for (let i = start; i <= end; i++) {
12 yield i; // Only computed when requested
13 }
14}
15
16// Eager: O(n) memory, all values computed immediately
17const allNumbers = eagerRange(1, 1000000); // Array of 1M items
18
19// Lazy: O(1) memory, values computed on demand
20const numbers = lazyRange(1, 1000000);
21// No computation happens yet!
22for (const n of numbers) {
23 console.log(n);
24 if (n > 10) break; // Only computes first 11 values
25}
26
27// The rest 989,989 values were never computed — huge savings
🔥

pro tip

Lazy evaluation with generators enables processing of potentially infinite data structures without memory overflow. You control how much computation occurs by controlling how many values you consume. This is the same principle behind streams in functional languages like Haskell — compute only what you need, when you need it.
Two-Way Communication with .next(value)

Generators support bidirectional data flow. While yield sends a value out to the caller, the .next(value) method can send a value back into the generator. This converts the yield expression into a receive channel — the value passed to .next() becomes the result of the pending yield expression inside the generator.

This is one of the most powerful features of generators. The first call to .next() cannot accept a value (there is no pending yield to receive it), but every subsequent call can inject data. This enables coroutine-like patterns where the generator and the caller exchange messages.

two-way.js
JavaScript
1// Two-way communication
2function* twoWay() {
3 const a = yield "Give me A";
4 console.log("Received A:", a);
5 const b = yield "Give me B";
6 console.log("Received B:", b);
7 const c = yield "Give me C";
8 console.log("Received C:", c);
9 return "Done";
10}
11
12const gen = twoWay();
13
14// First next() — cannot send a value, no yield to receive yet
15console.log(gen.next()); // { value: "Give me A", done: false }
16
17// Send value "Hello" — becomes result of first yield
18console.log(gen.next("Hello")); // { value: "Give me B", done: false }
19// Logs: "Received A: Hello"
20
21// Send value "World" — becomes result of second yield
22console.log(gen.next("World")); // { value: "Give me C", done: false }
23// Logs: "Received B: World"
24
25// Send value "!" — becomes result of third yield
26console.log(gen.next("!")); // { value: "Done", done: true }
27// Logs: "Received C: !"
bank-account.js
JavaScript
1// Practical: controllable generator
2function* bankAccount() {
3 let balance = 0;
4 while (true) {
5 const action = yield balance;
6 if (action.type === "deposit") {
7 balance += action.amount;
8 } else if (action.type === "withdraw") {
9 if (action.amount <= balance) {
10 balance -= action.amount;
11 } else {
12 console.log("Insufficient funds");
13 }
14 } else if (action.type === "reset") {
15 balance = 0;
16 }
17 }
18}
19
20const account = bankAccount();
21account.next(); // Start the generator, balance is 0
22
23console.log(account.next({ type: "deposit", amount: 100 }).value); // 100
24console.log(account.next({ type: "deposit", amount: 50 }).value); // 150
25console.log(account.next({ type: "withdraw", amount: 30 }).value); // 120
26console.log(account.next({ type: "reset" }).value); // 0
27
28// The generator maintains state across pauses
29// and communicates with the outside world through yield/next

best practice

The two-way communication pattern is what makes generators suitable for coroutines and cooperative multitasking. Libraries like Redux-Saga use this to manage complex side-effect flows — the generator yields effects (like API calls), and the middleware executes them and sends the result back via .next().
Generator Delegation (yield*)

The yield* expression delegates to another generator or any iterable. It yields all values from the inner iterable within the outer generator. This is analogous to the spread operator but with lazy semantics — values are produced on demand rather than consumed all at once.

yield-star.js
JavaScript
1// Delegating to another generator
2function* inner() {
3 yield "inner 1";
4 yield "inner 2";
5}
6
7function* outer() {
8 yield "outer 1";
9 yield* inner();
10 yield "outer 2";
11}
12
13for (const val of outer()) {
14 console.log(val);
15}
16// "outer 1"
17// "inner 1"
18// "inner 2"
19// "outer 2"
20
21// Delegating to arrays and other iterables
22function* combine() {
23 yield* [1, 2, 3];
24 yield* "abc";
25 yield* new Set([4, 5, 6]);
26}
27
28console.log([...combine()]); // [1, 2, 3, "a", "b", "c", 4, 5, 6]
29
30// yield* accepts the return value of the inner generator
31function* innerWithReturn() {
32 yield 1;
33 yield 2;
34 return "done";
35}
36
37function* outerWithReturn() {
38 const result = yield* innerWithReturn();
39 console.log("Inner returned:", result); // "done"
40 yield 3;
41}
42
43console.log([...outerWithReturn()]); // [1, 2, 3]
recursive-delegation.js
JavaScript
1// Recursive generator using yield*
2function* deepKeys(obj, prefix = "") {
3 for (const [key, value] of Object.entries(obj)) {
4 const path = prefix ? `${prefix}.${key}` : key;
5 if (typeof value === "object" && value !== null && !Array.isArray(value)) {
6 yield* deepKeys(value, path);
7 } else {
8 yield path;
9 }
10 }
11}
12
13const nested = {
14 user: { name: "Alice", address: { city: "NYC", zip: "10001" } },
15 settings: { theme: "dark" },
16};
17
18for (const key of deepKeys(nested)) {
19 console.log(key);
20}
21// "user.name"
22// "user.address.city"
23// "user.address.zip"
24// "settings.theme"
25
26// Composing generators
27function* take(n, iterable) {
28 let i = 0;
29 for (const val of iterable) {
30 if (i++ >= n) return;
31 yield val;
32 }
33}
34
35function* map(fn, iterable) {
36 for (const val of iterable) {
37 yield fn(val);
38 }
39}
40
41function* filter(pred, iterable) {
42 for (const val of iterable) {
43 if (pred(val)) yield val;
44 }
45}
46
47// Composition — lazy pipeline, no intermediate arrays
48const pipeline = take(5, map(x => x * 2, filter(x => x % 2 === 0, [1,2,3,4,5,6,7,8,9,10])));
49console.log([...pipeline]); // [4, 8, 12, 16, 20]
🔥

pro tip

yield* is not just for generators — it works with any iterable. This makes it useful for flattening, composing, and transforming sequences without creating intermediate arrays. Combined with take, map, and filter lazy helpers, you can build expressive data pipelines.
Practical: Infinite Sequences

Because generators are lazy, they can represent infinite sequences. An infinite generator never returns { done: true } — it keeps yielding values forever. The consumer controls how many values to take, making infinite data structures practical.

infinite-sequences.js
JavaScript
1// Infinite counter
2function* infiniteCounter(start = 0) {
3 while (true) {
4 yield start++;
5 }
6}
7
8// Take helper — consume n values from any iterable
9function* take(n, iterable) {
10 let i = 0;
11 for (const val of iterable) {
12 if (i++ >= n) return;
13 yield val;
14 }
15}
16
17// Safe to use with infinite generators because of lazy evaluation
18const first10 = [...take(10, infiniteCounter(100))];
19console.log(first10); // [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
20
21// Infinite Fibonacci
22function* fibonacci() {
23 let a = 0, b = 1;
24 while (true) {
25 yield a;
26 [a, b] = [b, a + b];
27 }
28}
29
30const fib10 = [...take(10, fibonacci())];
31console.log(fib10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
32
33// Infinite primes (Sieve of Eratosthenes, lazy)
34function* primes() {
35 yield 2;
36 const sieve = new Map();
37 let candidate = 3;
38 while (true) {
39 const step = sieve.get(candidate);
40 if (step !== undefined) {
41 sieve.delete(candidate);
42 let next = candidate + step;
43 while (sieve.has(next)) next += step;
44 sieve.set(next, step);
45 } else {
46 yield candidate;
47 sieve.set(candidate * candidate, candidate * 2);
48 }
49 candidate += 2;
50 }
51}
52
53const firstPrimes = [...take(15, primes())];
54console.log(firstPrimes); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

warning

Always use a take or similar limiting mechanism when consuming infinite generators. Spreading ([...gen]) an infinite generator will cause an infinite loop and crash your program. The same applies to Array.from() and for...of without a break condition.
Practical: Async Generators

Async generators combine the generator pattern with promises. An async function* produces an AsyncGenerator that yields promises. Consumers use for await...of to consume values asynchronously. This is perfect for streaming data — paginated APIs, file readers, WebSocket messages, and database cursors.

async-generators.js
JavaScript
1// Async generator — yields promises
2async function* paginatedAPI(baseUrl, maxPages = 5) {
3 let page = 1;
4 let hasMore = true;
5
6 while (hasMore && page <= maxPages) {
7 const response = await fetch(`${baseUrl}?page=${page}`);
8 const data = await response.json();
9 yield data.items;
10 hasMore = data.hasMore;
11 page++;
12 }
13}
14
15// Usage with for await...of
16async function loadAllProducts() {
17 const results = [];
18 for await (const items of paginatedAPI("https://api.example.com/products")) {
19 results.push(...items);
20 console.log(`Loaded ${items.length} items, total: ${results.length}`);
21 }
22 return results;
23}
24
25// Async generator from event stream
26async function* fromEventEmitter(element, eventName) {
27 let resolve;
28 const queue = [];
29 const handler = (event) => {
30 if (resolve) {
31 resolve(event);
32 resolve = null;
33 } else {
34 queue.push(event);
35 }
36 };
37 element.addEventListener(eventName, handler);
38 try {
39 while (true) {
40 if (queue.length > 0) {
41 yield queue.shift();
42 } else {
43 yield new Promise((r) => { resolve = r; });
44 }
45 }
46 } finally {
47 element.removeEventListener(eventName, handler);
48 }
49}
50
51// Usage: consume click events lazily
52// for await (const click of fromEventEmitter(button, 'click')) {
53// console.log('Clicked at:', click.clientX, click.clientY);
54// if (someCondition) break;
55// }
async-streaming.js
JavaScript
1// Async generator for streaming file processing (Node.js)
2import { createReadStream } from "fs";
3import { createInterface } from "readline";
4
5async function* readLines(filePath) {
6 const stream = createReadStream(filePath);
7 const rl = createInterface({ input: stream, crlfDelay: Infinity });
8
9 for await (const line of rl) {
10 yield line;
11 }
12}
13
14// Process a large file line-by-line without loading into memory
15async function processLargeLog(filePath) {
16 let errorCount = 0;
17 for await (const line of readLines(filePath)) {
18 if (line.includes("ERROR")) {
19 errorCount++;
20 if (errorCount <= 10) {
21 console.log("Error line:", line);
22 }
23 }
24 }
25 console.log(`Total errors: ${errorCount}`);
26}
27
28// Async generator with timeout/retry
29async function* pollWithRetry(fn, interval = 1000, maxRetries = 3) {
30 for (let attempt = 0; attempt < maxRetries; attempt++) {
31 try {
32 const result = await fn();
33 yield result;
34 await new Promise(r => setTimeout(r, interval));
35 } catch (err) {
36 console.error(`Attempt ${attempt + 1} failed:`, err.message);
37 if (attempt === maxRetries - 1) throw err;
38 }
39 }
40}
41
42// Usage
43// for await (const status of pollWithRetry(() => checkJobStatus(jobId), 2000, 5)) {
44// console.log('Job status:', status);
45// if (status === 'completed' || status === 'failed') break;
46// }

info

Async generators are the natural JavaScript counterpart to streams in other languages. They compose well — you can pipe an async generator through map, filter, and take async helpers just like synchronous generators. Node.js Readable streams can be consumed as async iterables, and the Web Streams API also supports async iteration.
Practical: Custom Iterables

Generators make it trivial to create custom iterables. Instead of manually implementing [Symbol.iterator]() with an object that has a next() method, you can use a generator function to define the iteration logic. The generator handles the protocol automatically.

custom-iterables.js
JavaScript
1// Traditional iterable (manual)
2class RangeOld {
3 constructor(start, end) {
4 this.start = start;
5 this.end = end;
6 }
7 [Symbol.iterator]() {
8 let i = this.start;
9 const end = this.end;
10 return {
11 next() {
12 return i <= end
13 ? { value: i++, done: false }
14 : { value: undefined, done: true };
15 }
16 };
17 }
18}
19
20// Generator-based iterable (much simpler)
21class Range {
22 constructor(start, end) {
23 this.start = start;
24 this.end = end;
25 }
26 *[Symbol.iterator]() {
27 for (let i = this.start; i <= this.end; i++) {
28 yield i;
29 }
30 }
31}
32
33for (const n of new Range(1, 5)) {
34 console.log(n); // 1, 2, 3, 4, 5
35}
36
37// Custom iterable: tree traversal
38class TreeNode {
39 constructor(value, left = null, right = null) {
40 this.value = value;
41 this.left = left;
42 this.right = right;
43 }
44 *[Symbol.iterator]() {
45 yield this.value;
46 if (this.left) yield* this.left;
47 if (this.right) yield* this.right;
48 }
49 *inOrder() {
50 if (this.left) yield* this.left.inOrder();
51 yield this.value;
52 if (this.right) yield* this.right.inOrder();
53 }
54}
55
56const tree = new TreeNode(1,
57 new TreeNode(2, new TreeNode(4), new TreeNode(5)),
58 new TreeNode(3, new TreeNode(6), new TreeNode(7))
59);
60console.log([...tree]); // [1, 2, 4, 5, 3, 6, 7] (pre-order)
61console.log([...tree.inOrder()]); // [4, 2, 5, 1, 6, 3, 7]
🔥

pro tip

Writing *[Symbol.iterator]() { ... } as a generator method is the cleanest way to make any object iterable. It automatically handles the iterator protocol — no need to manually return an object with a next() method. This pattern works well for collections, trees, ranges, and any data structure you want to iterate over.
State Machine Pattern

Generators are ideal for implementing state machines. Each yield represents a state boundary — the generator pauses, receives input, and transitions to the next state. This maps naturally to finite state machines where states are explicit and transitions are triggered by external input.

state-machine.js
JavaScript
1// Traffic light state machine with generator
2function* trafficLight() {
3 const states = ["green", "yellow", "red"];
4 let index = 0;
5
6 while (true) {
7 const currentState = states[index % states.length];
8 const override = yield currentState;
9 if (override && states.includes(override)) {
10 index = states.indexOf(override);
11 } else {
12 index++;
13 }
14 }
15}
16
17const light = trafficLight();
18console.log(light.next().value); // "green"
19console.log(light.next().value); // "yellow"
20console.log(light.next().value); // "red"
21console.log(light.next().value); // "green"
22console.log(light.next("yellow").value); // "yellow" (override)
23console.log(light.next().value); // "red"
24
25// Form wizard state machine
26function* formWizard() {
27 const formData = {};
28
29 formData.name = yield "name";
30 console.log("Name:", formData.name);
31
32 formData.email = yield "email";
33 console.log("Email:", formData.email);
34
35 formData.age = yield "age";
36 console.log("Age:", formData.age);
37
38 const confirm = yield "confirm";
39 if (confirm === true) {
40 yield "submitted";
41 return formData;
42 }
43 yield "cancelled";
44 return null;
45}
46
47// Usage
48async function runWizard() {
49 const wizard = formWizard();
50 wizard.next(); // start
51
52 wizard.next("Alice"); // name
53 wizard.next("alice@example.com"); // email
54 wizard.next("30"); // age
55 const result = wizard.next(true); // confirm
56 console.log(result.value); // "submitted"
57 return result.value;
58}

info

Generator-based state machines are more readable than manual state-transition tables because the state is implicit in the execution position. Each yield is a breakpoint where the machine pauses and awaits external input. This pattern is used in libraries like Redux-Saga, where sagas are long-lived generator functions managing complex async flows.
$Blueprint — Engineering Documentation·Section ID: JS-GEN·Revision: 1.0