Back to Blog
JavaScriptGeneratorsIterationAsync

JavaScript Generators Guide

Master JavaScript generators. From basic iteration to async control flow to practical patterns.

B
Bootspring Team
Engineering
August 1, 2020
8 min read

Generators are functions that can pause and resume execution. Here's how to use them effectively.

Basic Generators#

1// Generator function syntax 2function* numberGenerator() { 3 yield 1; 4 yield 2; 5 yield 3; 6} 7 8const gen = numberGenerator(); 9 10console.log(gen.next()); // { value: 1, done: false } 11console.log(gen.next()); // { value: 2, done: false } 12console.log(gen.next()); // { value: 3, done: false } 13console.log(gen.next()); // { value: undefined, done: true } 14 15// Using for...of 16for (const num of numberGenerator()) { 17 console.log(num); // 1, 2, 3 18} 19 20// Spread operator 21const numbers = [...numberGenerator()]; // [1, 2, 3]

Yield Expressions#

1// yield returns values 2function* greetings() { 3 yield 'Hello'; 4 yield 'World'; 5 return 'Done'; // Return value only in final next() 6} 7 8const g = greetings(); 9console.log(g.next()); // { value: 'Hello', done: false } 10console.log(g.next()); // { value: 'World', done: false } 11console.log(g.next()); // { value: 'Done', done: true } 12 13// yield expressions can receive values 14function* conversation() { 15 const name = yield 'What is your name?'; 16 const age = yield `Hello ${name}, how old are you?`; 17 yield `${name} is ${age} years old`; 18} 19 20const conv = conversation(); 21console.log(conv.next().value); // 'What is your name?' 22console.log(conv.next('Alice').value); // 'Hello Alice, how old are you?' 23console.log(conv.next(25).value); // 'Alice is 25 years old'

Infinite Generators#

1// Infinite sequence 2function* infiniteCounter() { 3 let count = 0; 4 while (true) { 5 yield count++; 6 } 7} 8 9const counter = infiniteCounter(); 10console.log(counter.next().value); // 0 11console.log(counter.next().value); // 1 12console.log(counter.next().value); // 2 13 14// Fibonacci sequence 15function* fibonacci() { 16 let [prev, curr] = [0, 1]; 17 while (true) { 18 yield curr; 19 [prev, curr] = [curr, prev + curr]; 20 } 21} 22 23const fib = fibonacci(); 24console.log(fib.next().value); // 1 25console.log(fib.next().value); // 1 26console.log(fib.next().value); // 2 27console.log(fib.next().value); // 3 28console.log(fib.next().value); // 5 29 30// Take first n values 31function* take(iterable, n) { 32 let count = 0; 33 for (const item of iterable) { 34 if (count >= n) return; 35 yield item; 36 count++; 37 } 38} 39 40const first10Fib = [...take(fibonacci(), 10)]; 41// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Delegating Generators#

1// yield* delegates to another generator 2function* inner() { 3 yield 'a'; 4 yield 'b'; 5} 6 7function* outer() { 8 yield 1; 9 yield* inner(); 10 yield 2; 11} 12 13console.log([...outer()]); // [1, 'a', 'b', 2] 14 15// yield* with return value 16function* innerWithReturn() { 17 yield 'x'; 18 yield 'y'; 19 return 'inner done'; 20} 21 22function* outerWithReturn() { 23 const result = yield* innerWithReturn(); 24 yield `Result: ${result}`; 25} 26 27console.log([...outerWithReturn()]); // ['x', 'y', 'Result: inner done'] 28 29// Tree traversal 30function* traverse(node) { 31 yield node.value; 32 if (node.left) yield* traverse(node.left); 33 if (node.right) yield* traverse(node.right); 34} 35 36const tree = { 37 value: 1, 38 left: { 39 value: 2, 40 left: { value: 4 }, 41 right: { value: 5 }, 42 }, 43 right: { 44 value: 3, 45 }, 46}; 47 48console.log([...traverse(tree)]); // [1, 2, 4, 5, 3]

Generator Methods#

1// next() - resume execution 2// return() - end generator 3// throw() - throw error in generator 4 5function* controlledGenerator() { 6 try { 7 yield 1; 8 yield 2; 9 yield 3; 10 } finally { 11 console.log('Cleanup'); 12 } 13} 14 15const gen1 = controlledGenerator(); 16console.log(gen1.next()); // { value: 1, done: false } 17console.log(gen1.return('early')); // Cleanup, { value: 'early', done: true } 18 19// throw() 20function* errorHandlingGenerator() { 21 try { 22 yield 1; 23 yield 2; 24 } catch (error) { 25 console.log('Caught:', error.message); 26 yield 'recovered'; 27 } 28} 29 30const gen2 = errorHandlingGenerator(); 31console.log(gen2.next()); // { value: 1, done: false } 32gen2.throw(new Error('Oops')); // Caught: Oops 33console.log(gen2.next()); // { value: 'recovered', done: false }

Async Generators#

1// Async generator function 2async function* asyncNumbers() { 3 for (let i = 1; i <= 3; i++) { 4 await new Promise(resolve => setTimeout(resolve, 100)); 5 yield i; 6 } 7} 8 9// Using for-await-of 10async function main() { 11 for await (const num of asyncNumbers()) { 12 console.log(num); // 1, 2, 3 (with delays) 13 } 14} 15 16// Fetch pages 17async function* fetchPages(baseUrl) { 18 let page = 1; 19 let hasMore = true; 20 21 while (hasMore) { 22 const response = await fetch(`${baseUrl}?page=${page}`); 23 const data = await response.json(); 24 25 yield data.items; 26 27 hasMore = data.hasNextPage; 28 page++; 29 } 30} 31 32// Process async stream 33async function processAllPages() { 34 for await (const items of fetchPages('/api/data')) { 35 for (const item of items) { 36 processItem(item); 37 } 38 } 39}

Iterator Protocol#

1// Making objects iterable 2const range = { 3 from: 1, 4 to: 5, 5 6 *[Symbol.iterator]() { 7 for (let i = this.from; i <= this.to; i++) { 8 yield i; 9 } 10 }, 11}; 12 13console.log([...range]); // [1, 2, 3, 4, 5] 14 15// Iterable class 16class Range { 17 constructor(from, to) { 18 this.from = from; 19 this.to = to; 20 } 21 22 *[Symbol.iterator]() { 23 for (let i = this.from; i <= this.to; i++) { 24 yield i; 25 } 26 } 27} 28 29const r = new Range(1, 5); 30console.log([...r]); // [1, 2, 3, 4, 5] 31 32// Async iterable 33class AsyncRange { 34 constructor(from, to) { 35 this.from = from; 36 this.to = to; 37 } 38 39 async *[Symbol.asyncIterator]() { 40 for (let i = this.from; i <= this.to; i++) { 41 await new Promise(r => setTimeout(r, 100)); 42 yield i; 43 } 44 } 45}

Utility Functions#

1// Map over generator 2function* map(iterable, fn) { 3 for (const item of iterable) { 4 yield fn(item); 5 } 6} 7 8// Filter generator 9function* filter(iterable, predicate) { 10 for (const item of iterable) { 11 if (predicate(item)) { 12 yield item; 13 } 14 } 15} 16 17// Reduce generator 18function reduce(iterable, fn, initial) { 19 let accumulator = initial; 20 for (const item of iterable) { 21 accumulator = fn(accumulator, item); 22 } 23 return accumulator; 24} 25 26// Zip generators 27function* zip(...iterables) { 28 const iterators = iterables.map(i => i[Symbol.iterator]()); 29 30 while (true) { 31 const results = iterators.map(i => i.next()); 32 33 if (results.some(r => r.done)) { 34 return; 35 } 36 37 yield results.map(r => r.value); 38 } 39} 40 41// Usage 42const nums = [1, 2, 3, 4, 5]; 43const doubled = [...map(nums, x => x * 2)]; // [2, 4, 6, 8, 10] 44const evens = [...filter(nums, x => x % 2 === 0)]; // [2, 4] 45const sum = reduce(nums, (a, b) => a + b, 0); // 15 46 47console.log([...zip([1, 2, 3], ['a', 'b', 'c'])]); 48// [[1, 'a'], [2, 'b'], [3, 'c']]

Practical Patterns#

1// Pagination helper 2function* paginate(array, pageSize) { 3 for (let i = 0; i < array.length; i += pageSize) { 4 yield array.slice(i, i + pageSize); 5 } 6} 7 8const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 9for (const page of paginate(items, 3)) { 10 console.log(page); 11} 12// [1, 2, 3] 13// [4, 5, 6] 14// [7, 8, 9] 15// [10] 16 17// Chunked processing 18async function* processInChunks(items, chunkSize, processor) { 19 for (let i = 0; i < items.length; i += chunkSize) { 20 const chunk = items.slice(i, i + chunkSize); 21 const results = await Promise.all(chunk.map(processor)); 22 yield* results; 23 } 24} 25 26// State machine 27function* trafficLight() { 28 while (true) { 29 yield 'green'; 30 yield 'yellow'; 31 yield 'red'; 32 } 33} 34 35// ID generator 36function* idGenerator(prefix = '') { 37 let id = 0; 38 while (true) { 39 yield `${prefix}${++id}`; 40 } 41} 42 43const userId = idGenerator('user_'); 44console.log(userId.next().value); // 'user_1' 45console.log(userId.next().value); // 'user_2' 46 47// Retry mechanism 48async function* retry(fn, maxAttempts, delay) { 49 for (let attempt = 1; attempt <= maxAttempts; attempt++) { 50 try { 51 yield await fn(); 52 return; 53 } catch (error) { 54 if (attempt === maxAttempts) { 55 throw error; 56 } 57 yield { attempt, error, retrying: true }; 58 await new Promise(r => setTimeout(r, delay * attempt)); 59 } 60 } 61}

Coroutines#

1// Simple coroutine runner 2function run(generatorFn) { 3 const generator = generatorFn(); 4 5 function handle(result) { 6 if (result.done) return Promise.resolve(result.value); 7 8 return Promise.resolve(result.value) 9 .then(res => handle(generator.next(res))) 10 .catch(err => handle(generator.throw(err))); 11 } 12 13 return handle(generator.next()); 14} 15 16// Usage 17run(function* () { 18 const user = yield fetch('/api/user').then(r => r.json()); 19 const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json()); 20 console.log(posts); 21}); 22 23// Cancellable operations 24function createCancellable(generatorFn) { 25 let cancelled = false; 26 const generator = generatorFn(); 27 28 const promise = new Promise((resolve, reject) => { 29 function step(result) { 30 if (cancelled) { 31 reject(new Error('Cancelled')); 32 return; 33 } 34 35 if (result.done) { 36 resolve(result.value); 37 return; 38 } 39 40 Promise.resolve(result.value) 41 .then(value => step(generator.next(value))) 42 .catch(error => step(generator.throw(error))); 43 } 44 45 step(generator.next()); 46 }); 47 48 return { 49 promise, 50 cancel: () => { cancelled = true; }, 51 }; 52}

Memory Efficiency#

1// Lazy evaluation - doesn't create full array 2function* range(start, end) { 3 for (let i = start; i <= end; i++) { 4 yield i; 5 } 6} 7 8// This is memory efficient 9for (const n of range(1, 1000000)) { 10 if (n > 10) break; 11 console.log(n); 12} 13 14// vs array which allocates all memory upfront 15// const arr = Array.from({ length: 1000000 }, (_, i) => i + 1); 16 17// Lazy file reading 18async function* readLines(file) { 19 const reader = file.getReader(); 20 const decoder = new TextDecoder(); 21 let buffer = ''; 22 23 try { 24 while (true) { 25 const { done, value } = await reader.read(); 26 27 if (done) { 28 if (buffer) yield buffer; 29 break; 30 } 31 32 buffer += decoder.decode(value, { stream: true }); 33 const lines = buffer.split('\n'); 34 buffer = lines.pop() || ''; 35 36 for (const line of lines) { 37 yield line; 38 } 39 } 40 } finally { 41 reader.releaseLock(); 42 } 43}

Best Practices#

Usage: ✓ Use for lazy evaluation ✓ Use for infinite sequences ✓ Use for stateful iteration ✓ Use for async data streams Patterns: ✓ Combine with yield* for delegation ✓ Use try/finally for cleanup ✓ Handle errors appropriately ✓ Use async generators for I/O Performance: ✓ Prefer generators for large datasets ✓ Avoid unnecessary array allocations ✓ Use take/limit for infinite generators ✓ Clean up resources in finally Avoid: ✗ Complex control flow in generators ✗ Forgetting to handle done state ✗ Ignoring cleanup on early exit ✗ Overusing generators for simple cases

Conclusion#

Generators provide powerful iteration control with lazy evaluation. Use them for infinite sequences, stateful iteration, async data streams, and memory-efficient processing. Combine with yield* for delegation and async/await for asynchronous operations.

Share this article

Help spread the word about Bootspring