Generators are special functions that can be paused and resumed, allowing you to control iteration and create lazy evaluation patterns. They provide a powerful way to work with sequences of data, implement custom iterators, and manage asynchronous operations.
In this tutorial, we'll explore generator functions, the yield keyword, generator methods, async generators, and practical use cases for this powerful JavaScript feature.
Understanding Generators
Generators are functions that can be paused and resumed, producing multiple values over time.
Basic Generator Syntax
// Generator function declaration
function* generatorFunction() {
yield 'First';
yield 'Second';
yield 'Third';
}
// Create generator object
const generator = generatorFunction();
// Get values using next()
console.log(generator.next()); // { value: 'First', done: false }
console.log(generator.next()); // { value: 'Second', done: false }
console.log(generator.next()); // { value: 'Third', done: false }
console.log(generator.next()); // { value: undefined, done: true }
// Generator expression
const generatorExpression = function*() {
yield 1;
yield 2;
yield 3;
};
// Arrow generator (ES2022)
const arrowGenerator = *() => {
yield 'A';
yield 'B';
};
Generator with for...of
// Using for...of with generators
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3, 4, 5
}
// Spread operator with generators
const numbers = [...numberGenerator()];
console.log(numbers); // [1, 2, 3, 4, 5]
// Destructuring with generators
const [first, second, third] = numberGenerator();
console.log(first, second, third); // 1, 2, 3
// Array methods with generators
const doubled = [...numberGenerator()].map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
Yield Keyword
The yield keyword pauses generator execution and returns a value.
Basic Yield Usage
// Yield with values
function* valueGenerator() {
yield 'Hello';
yield 'World';
yield 'Generator';
}
// Yield with expressions
function* expressionGenerator() {
const x = 10;
yield x * 2; // 20
yield x + 5; // 15
yield Math.sqrt(x); // 3.162...
}
// Yield with function calls
function* functionGenerator() {
yield Math.random(); // Random number
yield Date.now(); // Current timestamp
yield new Date().toISOString(); // ISO date string
}
// Conditional yielding
function* conditionalGenerator(shouldYield) {
yield 'Always yielded';
if (shouldYield) {
yield 'Sometimes yielded';
}
yield 'Always yielded again';
}
Yield with Return Values
// Yield with return
function* returnGenerator() {
yield 'First value';
yield 'Second value';
return 'Final value'; // This becomes the last value
}
const gen = returnGenerator();
console.log(gen.next()); // { value: 'First value', done: false }
console.log(gen.next()); // { value: 'Second value', done: false }
console.log(gen.next()); // { value: 'Final value', done: true }
// Yield with return in loops
function* loopGenerator(limit) {
for (let i = 0; i < limit; i++) {
yield i;
}
return `Generated ${limit} numbers`;
}
const loopGen = loopGenerator(3);
console.log(loopGen.next()); // { value: 0, done: false }
console.log(loopGen.next()); // { value: 1, done: false }
console.log(loopGen.next()); // { value: 2, done: false }
console.log(loopGen.next()); // { value: 'Generated 3 numbers', done: true }
Generator Methods
Generator objects provide methods for controlling and interacting with generators.
next() Method
// next() with arguments
function* interactiveGenerator() {
const received = yield 'Waiting for input...';
yield `Received: ${received}`;
yield 'Done';
}
const gen = interactiveGenerator();
console.log(gen.next()); // { value: 'Waiting for input...', done: false }
console.log(gen.next('Hello World')); // { value: 'Received: Hello World', done: false }
console.log(gen.next()); // { value: 'Done', done: true }
// next() with error handling
function* errorHandlingGenerator() {
try {
const result = yield 'Process this';
yield `Success: ${result}`;
} catch (error) {
yield `Error: ${error.message}`;
}
}
const errorGen = errorHandlingGenerator();
console.log(errorGen.next()); // { value: 'Process this', done: false }
console.log(errorGen.throw(new Error('Something went wrong')));
// { value: 'Error: Something went wrong', done: false }
return() and throw() Methods
// return() method
function* returnMethodGenerator() {
yield 'First';
yield 'Second';
yield 'Third';
}
const gen = returnMethodGenerator();
console.log(gen.next()); // { value: 'First', done: false }
console.log(gen.return('Early return')); // { value: 'Early return', done: true }
console.log(gen.next()); // { value: undefined, done: true }
// throw() method
function* throwMethodGenerator() {
try {
yield 'First';
yield 'Second';
} catch (error) {
yield `Caught: ${error.message}`;
}
yield 'After catch';
}
const throwGen = throwMethodGenerator();
console.log(throwGen.next()); // { value: 'First', done: false }
console.log(throwGen.throw(new Error('Custom error')));
// { value: 'Caught: Custom error', done: false }
console.log(throwGen.next()); // { value: 'After catch', done: false }
Advanced Generator Patterns
Sophisticated patterns using generators for complex functionality.
Delegating Generators
// yield* for delegating to other generators
function* innerGenerator() {
yield 'Inner 1';
yield 'Inner 2';
}
function* outerGenerator() {
yield 'Outer 1';
yield* innerGenerator(); // Delegate to inner generator
yield 'Outer 2';
}
for (const value of outerGenerator()) {
console.log(value); // 'Outer 1', 'Inner 1', 'Inner 2', 'Outer 2'
}
// yield* with arrays
function* arrayGenerator() {
yield* [1, 2, 3]; // Yields each array element
yield* ['a', 'b', 'c'];
}
// yield* with strings
function* stringGenerator() {
yield* 'Hello'; // Yields each character
}
for (const char of stringGenerator()) {
console.log(char); // 'H', 'e', 'l', 'l', 'o'
}
// yield* with other iterables
function* iterableGenerator() {
yield* new Set([1, 2, 3]);
yield* new Map([['a', 1], ['b', 2]]).keys();
}
Generator Composers
// Compose generators
function* compose(...generators) {
for (const gen of generators) {
yield* gen;
}
}
function* numbers1to3() {
yield 1; yield 2; yield 3;
}
function* lettersAToC() {
yield 'a'; yield 'b'; yield 'c';
}
for (const value of compose(numbers1to3(), lettersAToC())) {
console.log(value); // 1, 2, 3, 'a', 'b', 'c'
}
// Generator transformer
function* map(generator, fn) {
for (const value of generator) {
yield fn(value);
}
}
function* filter(generator, predicate) {
for (const value of generator) {
if (predicate(value)) {
yield value;
}
}
}
// Usage
function* numberSequence() {
let i = 1;
while (i <= 10) {
yield i++;
}
}
const doubled = map(numberSequence(), x => x * 2);
const evens = filter(numberSequence(), x => x % 2 === 0);
console.log([...doubled]); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
console.log([...evens]); // [2, 4, 6, 8, 10]
Practical Generator Use Cases
Real-world applications of generators in JavaScript development.
Infinite Sequences
// Fibonacci sequence generator
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield prev;
[prev, curr] = [curr, prev + curr];
}
}
// Get first 10 Fibonacci numbers
const fib = fibonacci();
const fibNumbers = [];
for (let i = 0; i < 10; i++) {
fibNumbers.push(fib.next().value);
}
console.log(fibNumbers); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Prime number generator
function* primes() {
const numbers = [];
for (let n = 2; ; n++) {
if (numbers.every(p => n % p !== 0)) {
numbers.push(n);
yield n;
}
}
}
// Get first 10 primes
const primeGen = primes();
const primeNumbers = [];
for (let i = 0; i < 10; i++) {
primeNumbers.push(primeGen.next().value);
}
console.log(primeNumbers); // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
// Random number generator
function* randomNumbers(min = 0, max = 100) {
while (true) {
yield Math.floor(Math.random() * (max - min + 1)) + min;
}
}
const randomGen = randomNumbers(1, 6); // Dice rolls
console.log(randomGen.next().value); // Random 1-6
Lazy Evaluation
// Lazy file reader
function* fileLineReader(file) {
const lines = file.split('\n');
for (const line of lines) {
yield line.trim();
}
}
// Process large file line by line
const largeFile = `Line 1\nLine 2\nLine 3\nLine 4\nLine 5`;
const lineReader = fileLineReader(largeFile);
for (const line of lineReader) {
console.log(`Processing: ${line}`);
// Only processes one line at a time
}
// Lazy range generator
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
// Process large ranges efficiently
const largeRange = range(1, 1000000);
const first100 = [];
for (const num of largeRange) {
first100.push(num);
if (first100.length >= 100) break;
}
// Lazy tree traversal
function* treeTraversal(node, type = 'inorder') {
if (type === 'preorder') {
yield node.value;
}
if (node.left) yield* treeTraversal(node.left, type);
if (node.right) yield* treeTraversal(node.right, type);
if (type === 'inorder') {
yield node.value;
}
if (type === 'postorder') {
yield node.value;
}
}
Async Generators
Async generators combine generators with asynchronous operations.
Async Generator Syntax
// Async generator function
async function* asyncGenerator() {
yield await fetch('/api/data1');
yield await fetch('/api/data2');
yield await fetch('/api/data3');
}
// Consuming async generator
async function processAsyncData() {
const gen = asyncGenerator();
for await (const data of gen) {
const json = await data.json();
console.log('Received:', json);
}
}
// Async generator with delays
async function* delayedSequence() {
let i = 1;
while (i <= 5) {
yield i;
await new Promise(resolve => setTimeout(resolve, 1000));
i++;
}
}
// Usage
async function runDelayedSequence() {
for await (const num of delayedSequence()) {
console.log(`Number: ${num}`);
}
}
// Async generator with error handling
async function* safeAsyncRequests(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
yield { url, data: await response.json(), error: null };
} catch (error) {
yield { url, data: null, error: error.message };
}
}
}
Practical Async Patterns
// Async rate limiter
async function* rateLimiter(requests, delay = 1000) {
for (const request of requests) {
const result = await fetch(request.url);
yield await result.json();
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// Usage
const apiRequests = [
{ url: '/api/user/1' },
{ url: '/api/user/2' },
{ url: '/api/user/3' }
];
async function processWithRateLimit() {
for await (const data of rateLimiter(apiRequests, 500)) {
console.log('Processed:', data);
}
}
// Async retry mechanism
async function* retryOperation(operation, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await operation();
yield { success: true, data: result, attempt };
return; // Success, exit generator
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
yield { retry: true, attempt, error: error.message };
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
yield { success: false, error: lastError, attempts: maxRetries };
}
Generators and Iteration
Generators implement the iterator protocol, making them work with JavaScript's iteration features.
Custom Iterators
// Object with generator as iterator
const iterableObject = {
items: ['a', 'b', 'c', 'd', 'e'],
*[Symbol.iterator]() {
for (const item of this.items) {
yield item.toUpperCase();
}
}
};
for (const item of iterableObject) {
console.log(item); // 'A', 'B', 'C', 'D', 'E'
}
// Tree with generator iterator
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
*[Symbol.iterator]() {
if (this.left) yield* this.left;
yield this.value;
if (this.right) yield* this.right;
}
}
// Usage
const tree = new TreeNode(2,
new TreeNode(1),
new TreeNode(3, new TreeNode(4), new TreeNode(5))
);
for (const value of tree) {
console.log(value); // 1, 2, 3, 4, 5
}
Generator Utilities
// Take first n items
function* take(generator, n) {
let i = 0;
for (const value of generator) {
if (i >= n) break;
yield value;
i++;
}
}
// Skip first n items
function* skip(generator, n) {
let i = 0;
for (const value of generator) {
if (i >= n) yield value;
i++;
}
}
// Take while condition is true
function* takeWhile(generator, predicate) {
for (const value of generator) {
if (!predicate(value)) break;
yield value;
}
}
// Usage with infinite generator
function* naturalNumbers() {
let i = 1;
while (true) yield i++;
}
console.log([...take(naturalNumbers(), 5)]); // [1, 2, 3, 4, 5]
console.log([...skip(naturalNumbers(), 5)]); // [6, 7, 8, 9, 10]
console.log([...takeWhile(naturalNumbers(), x => x < 10)]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Chunk generator
function* chunk(generator, size) {
let chunk = [];
for (const value of generator) {
chunk.push(value);
if (chunk.length === size) {
yield chunk;
chunk = [];
}
}
if (chunk.length > 0) yield chunk;
}
console.log([...chunk(naturalNumbers(), 3)]); // [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
Performance Considerations
Understanding performance characteristics of generators.
Memory Efficiency
// Generators are memory efficient for large datasets
function* largeDataset() {
for (let i = 0; i < 1000000; i++) {
yield {
id: i,
data: `Item ${i}`,
timestamp: Date.now()
};
}
}
// Process without loading everything into memory
function processLargeDataset() {
let processed = 0;
for (const item of largeDataset()) {
// Process one item at a time
processed++;
if (processed >= 100) break; // Stop after 100 items
}
return processed;
}
// Compare with array approach (memory intensive)
function arrayApproach() {
const items = [];
for (let i = 0; i < 1000000; i++) {
items.push({
id: i,
data: `Item ${i}`,
timestamp: Date.now()
});
}
return items; // All items in memory at once
}
// Lazy evaluation benefits
function* lazyFilter(generator, predicate) {
for (const value of generator) {
if (predicate(value)) yield value;
}
}
// Chain operations without intermediate arrays
const result = [...take(
lazyFilter(
lazyFilter(naturalNumbers(), x => x % 2 === 0), // Even numbers
x => x % 3 === 0 // Divisible by 3
),
5
)];
// Only processes what's needed
Performance Measurement
// Measure generator vs array performance
function measurePerformance() {
const iterations = 1000000;
// Array approach
const arrayStart = performance.now();
const array = [];
for (let i = 0; i < iterations; i++) {
array.push(i);
}
const arrayEnd = performance.now();
// Generator approach
const genStart = performance.now();
const gen = numberGenerator();
let count = 0;
for (const value of gen) {
count++;
if (count >= iterations) break;
}
const genEnd = performance.now();
console.log(`Array creation: ${arrayEnd - arrayStart}ms`);
console.log(`Generator iteration: ${genEnd - genStart}ms`);
}
// Memory usage comparison
function memoryComparison() {
// Array memory usage
const array = Array.from({length: 100000}, (_, i) => i);
const arrayMemory = array.length * 8; // Rough estimate
// Generator memory usage (minimal)
function* numberGenerator() {
let i = 0;
while (i < 100000) yield i++;
}
const gen = numberGenerator();
const genMemory = 24; // Rough estimate for generator object
console.log(`Array memory: ~${arrayMemory} bytes`);
console.log(`Generator memory: ~${genMemory} bytes`);
}
Common Generator Pitfalls
Avoid these common mistakes when working with generators.
Iteration Issues
// Pitfall: Using generator after completion
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
console.log(gen.next()); // { value: undefined, done: true } (still done)
// Pitfall: Mixing iteration methods
const gen2 = simpleGenerator();
console.log([...gen2]); // [1, 2, 3]
console.log(gen2.next()); // { value: undefined, done: true }
// Pitfall: Modifying generator during iteration
function* modifiableGenerator() {
let i = 0;
while (i < 5) {
yield i++;
}
}
const gen3 = modifiableGenerator();
for (const value of gen3) {
console.log(value);
// gen3.next() // Don't call next() during for...of
}
Async/Await Issues
// Pitfall: Using await in sync generator
function* badAsyncGenerator() {
const data = yield fetch('/api/data'); // yield doesn't wait for promise
// data is a promise, not the response
}
// Correct: Use async generator
async function* goodAsyncGenerator() {
const response = yield fetch('/api/data'); // Still needs await
const data = await response.json();
yield data;
}
// Better: Use await with yield
async function* bestAsyncGenerator() {
const response = await fetch('/api/data');
const data = await response.json();
yield data;
}
// Pitfall: Forgetting for await
async function consumeAsyncGenerator() {
const gen = asyncGenerator();
// Wrong: regular for...of
for (const value of gen) {
console.log(value); // Yields promises, not values
}
// Correct: for await...of
for await (const value of gen) {
console.log(value); // Yields resolved values
}
}
Best Practices
Guidelines for using generators effectively and safely.
Code Organization
// Use generators for lazy evaluation
function* readFileLines(filePath) {
const content = require('fs').readFileSync(filePath, 'utf8');
const lines = content.split('\n');
for (const line of lines) {
yield line.trim();
}
}
// Use generators for state machines
function* stateMachine() {
let state = 'idle';
while (true) {
const action = yield state;
switch (state) {
case 'idle':
if (action === 'start') state = 'running';
break;
case 'running':
if (action === 'pause') state = 'paused';
if (action === 'stop') state = 'idle';
break;
case 'paused':
if (action === 'resume') state = 'running';
if (action === 'stop') state = 'idle';
break;
}
}
}
// Use generators for data pipelines
function* pipeline(data, ...processors) {
let result = data;
for (const processor of processors) {
result = processor(result);
yield result;
}
}
// Usage
const text = "Hello World";
const processors = [
s => s.toLowerCase(),
s => s.split(' '),
s => s.map(w => w[0].toUpperCase() + w.slice(1)),
s => s.join(' ')
];
for (const step of pipeline(text, ...processors)) {
console.log(step);
}
Error Handling
// Robust error handling in generators
function* robustGenerator() {
try {
yield 'Step 1';
yield 'Step 2';
yield 'Step 3';
} catch (error) {
console.error('Generator error:', error);
yield `Error: ${error.message}`;
} finally {
console.log('Generator cleanup');
yield 'Cleanup complete';
}
}
// Safe generator consumption
function safeConsume(generator) {
const results = [];
try {
for (const value of generator) {
results.push(value);
}
} catch (error) {
console.error('Consumption error:', error);
}
return results;
}
// Async generator error handling
async function* robustAsyncGenerator(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
const data = await response.json();
yield { url, data, error: null };
} catch (error) {
yield { url, data: null, error: error.message };
}
}
}
Summary
Key Takeaways
- Generators can be paused and resumed during execution
- yield keyword returns values and pauses execution
- Generators implement the iterator protocol
- Async generators work with asynchronous operations
- Generators enable lazy evaluation and memory efficiency
Best Practices
- Use generators for large datasets and infinite sequences
- Implement proper error handling in generators
- Use yield* for delegating to other iterables
- Use for await...of with async generators
- Consider memory efficiency when choosing generators
Common Pitfalls
- Forgetting that generators are one-time use
- Mixing regular for...of with async generators
- Not handling generator completion properly
- Using await incorrectly in sync generators
- Modifying generator during iteration