iTranslated by AI
Finally Understanding JavaScript Generator Functions
How I Finally Understood JavaScript Generator Functions
I’ve always been confused by "what is function*?" and "what is yield?", but today I finally realized, "Hey, this is actually convenient!" so I’m writing it down.
What is a generator function?
A regular function runs to the end and finishes when called, but a generator function is a function that can pause in the middle.
function* counter() {
yield 1;
yield 2;
yield 3;
}
const gen = counter();
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 }
It pauses at yield and waits until .next() is called again.
Why is this useful?
At first, I thought, "Isn't a regular array fine?" But being able to create infinite sequences is powerful.
function* infiniteId() {
let id = 1;
while (true) {
yield id++;
}
}
const gen = infiniteId();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// It works no matter how many times you call it, and unlike arrays, it doesn't exhaust memory
If you created an infinite list with an array, you'd run out of memory, but with a generator, you can generate only what you need.
Practical example: Pagination
Writing API pagination with a generator makes the code much cleaner.
async function* fetchAllUsers(baseUrl) {
let page = 1;
while (true) {
const res = await fetch(`${baseUrl}?page=${page}`);
const data = await res.json();
if (data.users.length === 0) break;
yield data.users; // Return users for this page
page++;
}
}
// Consuming side
for await (const users of fetchAllUsers('/api/users')) {
console.log('Users on this page:', users);
// You can break here if the processing is heavy
}
Writing this with a standard loop makes managing while(true) + break tedious, but using a generator keeps the logic in one place.
Practical example: Multi-step processes
This is what made me think, "I see!" today.
Processes that span multiple steps, like animations or wizard-style forms, tend to become complex if written with classes or state machines.
// ❌ If written as a state machine...
class WizardForm {
private step = 0;
private data: Partial<FormData> = {};
next(input: string) {
switch (this.step) {
case 0:
this.data.name = input;
this.step++;
return 'Please enter your age';
case 1:
this.data.age = parseInt(input);
this.step++;
return 'Please enter your occupation';
case 2:
this.data.job = input;
return `Completed! ${JSON.stringify(this.data)}`;
}
}
}
Every time a step is added, the switch block grows...
// ✅ If written with a generator...
function* wizardForm() {
const name = yield 'Please enter your name';
const age = yield 'Please enter your age';
const job = yield 'Please enter your occupation';
return `Completed! ${JSON.stringify({ name, age, job })}`;
}
const wizard = wizardForm();
console.log(wizard.next().value); // "Please enter your name"
console.log(wizard.next('Hina').value); // "Please enter your age"
console.log(wizard.next('21').value); // "Please enter your occupation"
console.log(wizard.next('Apprentice Engineer')); // { value: "Completed!...", done: true }
The order of the steps flows naturally from top to bottom! It's so easy to read.
By the way, the point is that you can pass values to yield, and the value passed via next(value) becomes the return value of the yield expression.
Typing in TypeScript
function* typedCounter(): Generator<number, string, boolean> {
// ↑Yield type ↑Return type ↑Type passed to next()
let count = 0;
while (true) {
const reset = yield count++;
if (reset) {
count = 0;
}
}
}
You can define types using three arguments: Generator<YieldType, ReturnType, NextType>.
Can also be used with for...of
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for (const n of range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
// Spread syntax also works
const arr = [...range(1, 5)]; // [1, 2, 3, 4, 5]
Since it implements Symbol.iterator, it can be treated as an iterable.
Conclusion
I avoided generator functions at first, thinking they were "difficult," but once you know where to use them, they are very convenient!
- Infinite sequences: You can create things that arrays cannot.
- Lazy evaluation: Generates values only when needed, saving memory.
- Multi-step processing: Easier to read than a state machine.
-
Asynchronous iteration: API pagination is cleaner with
async function*+for await...of.
The feeling of the code stopping at yield felt strange at first, but once you get used to it, it might actually be more intuitive!
Next, I want to try delegating to another generator using yield* 📖
Discussion