JavaScript — Generators & Iteration
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.
| 1 | // A simple generator function |
| 2 | function* counter() { |
| 3 | yield 1; |
| 4 | yield 2; |
| 5 | yield 3; |
| 6 | } |
| 7 | |
| 8 | const gen = counter(); |
| 9 | console.log(gen.next()); // { value: 1, done: false } |
| 10 | console.log(gen.next()); // { value: 2, done: false } |
| 11 | console.log(gen.next()); // { value: 3, done: false } |
| 12 | console.log(gen.next()); // { value: undefined, done: true } |
| 13 | |
| 14 | // Generators are iterable |
| 15 | for (const value of counter()) { |
| 16 | console.log(value); // 1, 2, 3 |
| 17 | } |
| 18 | |
| 19 | // Spread works on generators |
| 20 | const all = [...counter()]; // [1, 2, 3] |
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
| Form | Syntax | Usage |
|---|---|---|
| Declaration | function* gen() | Named generator function |
| Expression | const gen = function*() | Anonymous generator expression |
| Method | const obj = { *gen() {} } | Generator as object method |
| Class | class Foo { *gen() {} } | Generator on class prototype |
| Arrow | N/A | Arrow functions cannot be generators |
| 1 | // Various declaration forms |
| 2 | function* countUp(limit) { |
| 3 | for (let i = 1; i <= limit; i++) { |
| 4 | yield i; |
| 5 | } |
| 6 | } |
| 7 | |
| 8 | // Generator expression |
| 9 | const countDown = function*(start) { |
| 10 | for (let i = start; i > 0; i--) { |
| 11 | yield i; |
| 12 | } |
| 13 | }; |
| 14 | |
| 15 | // Generator as object method |
| 16 | const 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 |
| 27 | class 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 |
| 38 | function* conditionalYield() { |
| 39 | yield 1; |
| 40 | if (Math.random() > 0.5) { |
| 41 | yield 2; |
| 42 | } |
| 43 | yield 3; |
| 44 | } |
info
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.
| 1 | // Demonstrating lazy execution |
| 2 | function* 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 | |
| 12 | console.log("Before calling generator"); |
| 13 | const gen = lazySequence(); |
| 14 | console.log("Generator created, nothing executed yet"); |
| 15 | |
| 16 | console.log(gen.next()); |
| 17 | // "Generator started" |
| 18 | // { value: 1, done: false } |
| 19 | |
| 20 | console.log(gen.next()); |
| 21 | // "Resumed after yield 1" |
| 22 | // { value: 2, done: false } |
| 23 | |
| 24 | console.log(gen.next()); |
| 25 | // "Resumed after yield 2" |
| 26 | // { value: 3, done: false } |
| 27 | |
| 28 | console.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 |
| 1 | // Lazy evaluation vs eager |
| 2 | function 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 | |
| 10 | function* 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 |
| 17 | const allNumbers = eagerRange(1, 1000000); // Array of 1M items |
| 18 | |
| 19 | // Lazy: O(1) memory, values computed on demand |
| 20 | const numbers = lazyRange(1, 1000000); |
| 21 | // No computation happens yet! |
| 22 | for (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
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.
| 1 | // Two-way communication |
| 2 | function* 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 | |
| 12 | const gen = twoWay(); |
| 13 | |
| 14 | // First next() — cannot send a value, no yield to receive yet |
| 15 | console.log(gen.next()); // { value: "Give me A", done: false } |
| 16 | |
| 17 | // Send value "Hello" — becomes result of first yield |
| 18 | console.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 |
| 22 | console.log(gen.next("World")); // { value: "Give me C", done: false } |
| 23 | // Logs: "Received B: World" |
| 24 | |
| 25 | // Send value "!" — becomes result of third yield |
| 26 | console.log(gen.next("!")); // { value: "Done", done: true } |
| 27 | // Logs: "Received C: !" |
| 1 | // Practical: controllable generator |
| 2 | function* 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 | |
| 20 | const account = bankAccount(); |
| 21 | account.next(); // Start the generator, balance is 0 |
| 22 | |
| 23 | console.log(account.next({ type: "deposit", amount: 100 }).value); // 100 |
| 24 | console.log(account.next({ type: "deposit", amount: 50 }).value); // 150 |
| 25 | console.log(account.next({ type: "withdraw", amount: 30 }).value); // 120 |
| 26 | console.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 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.
| 1 | // Delegating to another generator |
| 2 | function* inner() { |
| 3 | yield "inner 1"; |
| 4 | yield "inner 2"; |
| 5 | } |
| 6 | |
| 7 | function* outer() { |
| 8 | yield "outer 1"; |
| 9 | yield* inner(); |
| 10 | yield "outer 2"; |
| 11 | } |
| 12 | |
| 13 | for (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 |
| 22 | function* combine() { |
| 23 | yield* [1, 2, 3]; |
| 24 | yield* "abc"; |
| 25 | yield* new Set([4, 5, 6]); |
| 26 | } |
| 27 | |
| 28 | console.log([...combine()]); // [1, 2, 3, "a", "b", "c", 4, 5, 6] |
| 29 | |
| 30 | // yield* accepts the return value of the inner generator |
| 31 | function* innerWithReturn() { |
| 32 | yield 1; |
| 33 | yield 2; |
| 34 | return "done"; |
| 35 | } |
| 36 | |
| 37 | function* outerWithReturn() { |
| 38 | const result = yield* innerWithReturn(); |
| 39 | console.log("Inner returned:", result); // "done" |
| 40 | yield 3; |
| 41 | } |
| 42 | |
| 43 | console.log([...outerWithReturn()]); // [1, 2, 3] |
| 1 | // Recursive generator using yield* |
| 2 | function* 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 | |
| 13 | const nested = { |
| 14 | user: { name: "Alice", address: { city: "NYC", zip: "10001" } }, |
| 15 | settings: { theme: "dark" }, |
| 16 | }; |
| 17 | |
| 18 | for (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 |
| 27 | function* take(n, iterable) { |
| 28 | let i = 0; |
| 29 | for (const val of iterable) { |
| 30 | if (i++ >= n) return; |
| 31 | yield val; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | function* map(fn, iterable) { |
| 36 | for (const val of iterable) { |
| 37 | yield fn(val); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | function* 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 |
| 48 | const pipeline = take(5, map(x => x * 2, filter(x => x % 2 === 0, [1,2,3,4,5,6,7,8,9,10]))); |
| 49 | console.log([...pipeline]); // [4, 8, 12, 16, 20] |
pro tip
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.
| 1 | // Infinite counter |
| 2 | function* infiniteCounter(start = 0) { |
| 3 | while (true) { |
| 4 | yield start++; |
| 5 | } |
| 6 | } |
| 7 | |
| 8 | // Take helper — consume n values from any iterable |
| 9 | function* 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 |
| 18 | const first10 = [...take(10, infiniteCounter(100))]; |
| 19 | console.log(first10); // [100, 101, 102, 103, 104, 105, 106, 107, 108, 109] |
| 20 | |
| 21 | // Infinite Fibonacci |
| 22 | function* fibonacci() { |
| 23 | let a = 0, b = 1; |
| 24 | while (true) { |
| 25 | yield a; |
| 26 | [a, b] = [b, a + b]; |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | const fib10 = [...take(10, fibonacci())]; |
| 31 | console.log(fib10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] |
| 32 | |
| 33 | // Infinite primes (Sieve of Eratosthenes, lazy) |
| 34 | function* 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 | |
| 53 | const firstPrimes = [...take(15, primes())]; |
| 54 | console.log(firstPrimes); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] |
warning
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.
| 1 | // Async generator — yields promises |
| 2 | async 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 |
| 16 | async 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 |
| 26 | async 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 | // } |
| 1 | // Async generator for streaming file processing (Node.js) |
| 2 | import { createReadStream } from "fs"; |
| 3 | import { createInterface } from "readline"; |
| 4 | |
| 5 | async 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 |
| 15 | async 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 |
| 29 | async 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
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.
| 1 | // Traditional iterable (manual) |
| 2 | class 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) |
| 21 | class 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 | |
| 33 | for (const n of new Range(1, 5)) { |
| 34 | console.log(n); // 1, 2, 3, 4, 5 |
| 35 | } |
| 36 | |
| 37 | // Custom iterable: tree traversal |
| 38 | class 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 | |
| 56 | const 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 | ); |
| 60 | console.log([...tree]); // [1, 2, 4, 5, 3, 6, 7] (pre-order) |
| 61 | console.log([...tree.inOrder()]); // [4, 2, 5, 1, 6, 3, 7] |
pro tip
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.
| 1 | // Traffic light state machine with generator |
| 2 | function* 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 | |
| 17 | const light = trafficLight(); |
| 18 | console.log(light.next().value); // "green" |
| 19 | console.log(light.next().value); // "yellow" |
| 20 | console.log(light.next().value); // "red" |
| 21 | console.log(light.next().value); // "green" |
| 22 | console.log(light.next("yellow").value); // "yellow" (override) |
| 23 | console.log(light.next().value); // "red" |
| 24 | |
| 25 | // Form wizard state machine |
| 26 | function* 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 |
| 48 | async 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