Back to Blog
JavaScriptGeneratorsIteratorsAsync

JavaScript Generators and Iterators

Master generators and iterators in JavaScript. From lazy evaluation to async generators to custom iterables.

B
Bootspring Team
Engineering
April 14, 2021
7 min read

Generators create iterable sequences with lazy evaluation. Here's how to use them effectively.

Basic Generators#

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

Infinite Sequences#

1// Infinite counter 2function* infiniteCounter(start = 0) { 3 let count = start; 4 while (true) { 5 yield count++; 6 } 7} 8 9// Take first 5 10const counter = infiniteCounter(); 11for (let i = 0; i < 5; i++) { 12 console.log(counter.next().value); // 0, 1, 2, 3, 4 13} 14 15// Fibonacci sequence 16function* fibonacci() { 17 let [prev, curr] = [0, 1]; 18 while (true) { 19 yield curr; 20 [prev, curr] = [curr, prev + curr]; 21 } 22} 23 24// Get first 10 Fibonacci numbers 25function take(generator, n) { 26 const result = []; 27 for (let i = 0; i < n; i++) { 28 const { value, done } = generator.next(); 29 if (done) break; 30 result.push(value); 31 } 32 return result; 33} 34 35console.log(take(fibonacci(), 10)); 36// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Passing Values to Generators#

1// next() can pass values back 2function* conversation() { 3 const name = yield 'What is your name?'; 4 const age = yield `Hello ${name}! How old are you?`; 5 return `${name} is ${age} years old`; 6} 7 8const chat = conversation(); 9console.log(chat.next().value); // 'What is your name?' 10console.log(chat.next('John').value); // 'Hello John! How old are you?' 11console.log(chat.next(30).value); // 'John is 30 years old' 12 13// State machine 14function* trafficLight() { 15 while (true) { 16 yield 'red'; 17 yield 'green'; 18 yield 'yellow'; 19 } 20} 21 22const light = trafficLight(); 23console.log(light.next().value); // 'red' 24console.log(light.next().value); // 'green' 25console.log(light.next().value); // 'yellow' 26console.log(light.next().value); // 'red'

Generator Delegation#

1// yield* delegates to another generator 2function* innerGenerator() { 3 yield 'a'; 4 yield 'b'; 5} 6 7function* outerGenerator() { 8 yield 1; 9 yield* innerGenerator(); 10 yield 2; 11} 12 13console.log([...outerGenerator()]); // [1, 'a', 'b', 2] 14 15// Recursive tree traversal 16function* traverse(node) { 17 yield node.value; 18 for (const child of node.children || []) { 19 yield* traverse(child); 20 } 21} 22 23const tree = { 24 value: 1, 25 children: [ 26 { value: 2, children: [{ value: 4 }, { value: 5 }] }, 27 { value: 3, children: [{ value: 6 }] }, 28 ], 29}; 30 31console.log([...traverse(tree)]); // [1, 2, 4, 5, 3, 6]

Custom Iterables#

1// Make object iterable 2const range = { 3 start: 1, 4 end: 5, 5 6 [Symbol.iterator]() { 7 let current = this.start; 8 const end = this.end; 9 10 return { 11 next() { 12 if (current <= end) { 13 return { value: current++, done: false }; 14 } 15 return { done: true }; 16 }, 17 }; 18 }, 19}; 20 21for (const num of range) { 22 console.log(num); // 1, 2, 3, 4, 5 23} 24 25// Using generator syntax 26const rangeGenerator = { 27 start: 1, 28 end: 5, 29 30 *[Symbol.iterator]() { 31 for (let i = this.start; i <= this.end; i++) { 32 yield i; 33 } 34 }, 35}; 36 37console.log([...rangeGenerator]); // [1, 2, 3, 4, 5]

Async Generators#

1// Async generator 2async function* fetchPages(url) { 3 let page = 1; 4 let hasMore = true; 5 6 while (hasMore) { 7 const response = await fetch(`${url}?page=${page}`); 8 const data = await response.json(); 9 10 yield data.items; 11 12 hasMore = data.hasMore; 13 page++; 14 } 15} 16 17// Iterate with for await...of 18async function processAllPages() { 19 for await (const items of fetchPages('/api/items')) { 20 for (const item of items) { 21 console.log(item); 22 } 23 } 24} 25 26// Stream processing 27async function* streamLines(url) { 28 const response = await fetch(url); 29 const reader = response.body.getReader(); 30 const decoder = new TextDecoder(); 31 let buffer = ''; 32 33 while (true) { 34 const { done, value } = await reader.read(); 35 36 if (done) { 37 if (buffer) yield buffer; 38 break; 39 } 40 41 buffer += decoder.decode(value, { stream: true }); 42 const lines = buffer.split('\n'); 43 buffer = lines.pop(); 44 45 for (const line of lines) { 46 yield line; 47 } 48 } 49}

Generator Utilities#

1// Map 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// Take first n items 18function* take(iterable, n) { 19 let count = 0; 20 for (const item of iterable) { 21 if (count >= n) break; 22 yield item; 23 count++; 24 } 25} 26 27// Skip first n items 28function* skip(iterable, n) { 29 let count = 0; 30 for (const item of iterable) { 31 if (count >= n) { 32 yield item; 33 } 34 count++; 35 } 36} 37 38// Chain generators 39function* chain(...iterables) { 40 for (const iterable of iterables) { 41 yield* iterable; 42 } 43} 44 45// Usage 46const numbers = function* () { 47 yield 1; 48 yield 2; 49 yield 3; 50 yield 4; 51 yield 5; 52}; 53 54const result = [ 55 ...take( 56 filter( 57 map(numbers(), x => x * 2), 58 x => x > 4 59 ), 60 2 61 ), 62]; 63console.log(result); // [6, 8]

Lazy Evaluation#

1// Lazy range 2function* range(start, end, step = 1) { 3 for (let i = start; i < end; i += step) { 4 yield i; 5 } 6} 7 8// Doesn't compute all values upfront 9const largeRange = range(0, 1000000); 10 11// Only computes needed values 12for (const num of take(filter(largeRange, x => x % 2 === 0), 5)) { 13 console.log(num); // 0, 2, 4, 6, 8 14} 15 16// Lazy file processing 17async function* readLargeFile(path) { 18 const file = await Deno.open(path); 19 const decoder = new TextDecoder(); 20 21 for await (const chunk of file.readable) { 22 yield decoder.decode(chunk); 23 } 24}

Generator for State Management#

1// Simple state machine 2function* gameState() { 3 let state = 'idle'; 4 5 while (true) { 6 const action = yield state; 7 8 switch (state) { 9 case 'idle': 10 if (action === 'start') state = 'playing'; 11 break; 12 case 'playing': 13 if (action === 'pause') state = 'paused'; 14 if (action === 'end') state = 'gameover'; 15 break; 16 case 'paused': 17 if (action === 'resume') state = 'playing'; 18 if (action === 'end') state = 'gameover'; 19 break; 20 case 'gameover': 21 if (action === 'restart') state = 'idle'; 22 break; 23 } 24 } 25} 26 27const game = gameState(); 28console.log(game.next().value); // 'idle' 29console.log(game.next('start').value); // 'playing' 30console.log(game.next('pause').value); // 'paused' 31console.log(game.next('resume').value); // 'playing'

Error Handling#

1// throw() method 2function* generator() { 3 try { 4 yield 1; 5 yield 2; 6 } catch (e) { 7 console.log('Caught:', e.message); 8 } 9 yield 3; 10} 11 12const gen = generator(); 13console.log(gen.next()); // { value: 1, done: false } 14console.log(gen.throw(new Error('Test'))); // Caught: Test, { value: 3, done: false } 15 16// return() method 17function* generator2() { 18 try { 19 yield 1; 20 yield 2; 21 } finally { 22 console.log('Cleanup'); 23 } 24} 25 26const gen2 = generator2(); 27console.log(gen2.next()); // { value: 1, done: false } 28console.log(gen2.return()); // Cleanup, { value: undefined, done: true }

Real-World Example: Pagination#

1// Paginated API client 2class PaginatedAPI { 3 constructor(baseUrl) { 4 this.baseUrl = baseUrl; 5 } 6 7 async *fetchAll(endpoint, pageSize = 20) { 8 let page = 1; 9 let hasMore = true; 10 11 while (hasMore) { 12 const response = await fetch( 13 `${this.baseUrl}${endpoint}?page=${page}&size=${pageSize}` 14 ); 15 const data = await response.json(); 16 17 for (const item of data.items) { 18 yield item; 19 } 20 21 hasMore = data.page < data.totalPages; 22 page++; 23 } 24 } 25} 26 27// Usage 28const api = new PaginatedAPI('https://api.example.com'); 29 30async function processAllUsers() { 31 for await (const user of api.fetchAll('/users')) { 32 console.log(user.name); 33 } 34}

Best Practices#

When to use: ✓ Large or infinite sequences ✓ Lazy evaluation needed ✓ Custom iteration logic ✓ Async data streams Performance: ✓ Use for lazy evaluation ✓ Avoid for small fixed arrays ✓ Clean up with return() ✓ Handle errors properly Patterns: ✓ Combine with async/await ✓ Use yield* for delegation ✓ Create reusable utilities ✓ Implement Symbol.iterator

Conclusion#

Generators enable lazy evaluation and custom iteration. Use them for large datasets, infinite sequences, and async streams. Combine with utility functions like map, filter, and take for powerful data processing pipelines.

Share this article

Help spread the word about Bootspring