Introduction
Now that we’ve covered the mechanics of generator functions, the next question is where to actually use them. They’re not a fit for every problem, but in a few specific scenarios they beat the alternatives clearly. Here are the ones I run into most.
Handling large data sets
One of the most common use cases for generator functions is when working with large datasets you don’t want to hold in memory all at once. For example, if you need to process a massive array of data, instead of loading the entire array into memory, you can use a generator to yield each item one at a time.
function* dataGenerator(dataArray) {
for (let i = 0; i < dataArray.length; i++) {
yield dataArray[i];
}
}
const data = [...Array(1000000).keys()]; // Large data array
const generator = dataGenerator(data);
let currentItem = generator.next();
while (!currentItem.done) {
console.log(currentItem.value);
currentItem = generator.next();
}
In this example, the dataGenerator function yields each item from the array one at a time, significantly reducing memory usage.
Managing asynchronous flow
As we mentioned earlier, generators can be handy for handling asynchronous operations. With generators, we can write asynchronous code that looks synchronous, thus improving code readability and maintainability.
Consider a case where multiple API calls must be made in a specific order. Using promises or callbacks can lead to deeply nested code (callback hell), but we can simplify this with generators.
function* fetchSequentialData() {
const users = yield fetch("/api/users").then(res => res.json());
const posts = yield fetch(`/api/users/${users[0].id}/posts`).then(res =>
res.json()
);
return posts;
}
const generator = fetchSequentialData();
generator
.next()
.value.then(data => generator.next(data).value)
.then(data => generator.next(data));
In this example, fetchSequentialData fetches user data, waits for it to resolve, and then fetches post data for the first user. This process occurs sequentially without deeply nesting the code.
Infinite data structures
Generators are also great for creating infinite data structures. Suppose you’re creating a game and need an endless supply of enemy characters. You could use a generator function to create enemies as you need them without any predefined limit.
function* enemyGenerator() {
let id = 0;
while (true) {
yield { id: id++, type: "enemy", hp: 100 };
}
}
const enemies = enemyGenerator();
console.log(enemies.next().value); // { id: 0, type: 'enemy', hp: 100 }
console.log(enemies.next().value); // { id: 1, type: 'enemy', hp: 100 }
// ... and so on, infinitely.
In this example, enemyGenerator it will create a new enemy each time it’s called and will do so indefinitely.
Generator functions aren’t the right tool for every job, but for large data sets, sequential asynchronous flow, and infinite data structures, they’re often the cleanest option available.