I ignored JavaScript generators for years. They looked weird, the docs were terrible, and every example showed Fibonacci ( which nobody actually writes ). Then I needed to process a 2GB CSV file in the browser without crashing the tab, and generators suddenly made sense.
Here is what actually matters about them.
The Basics in 30 Seconds
A generator is a function that can pause. You write function* instead of function, and use yield to return values one at a time. The caller resumes it with .next(). That is it.
function* countTo3() {
yield 1;
yield 2;
yield 3;
}
const gen = countTo3();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }No callbacks, no promises, no event loop tricks. Just pause and resume.
Why I Actually Use Them
Lazy iteration on large data. That is 90% of my use case. Instead of loading an entire array into memory, I yield one item at a time. The consumer decides when to stop.
// Process a huge file line by line
function* readLines(text) {
const lines = text.split("\n");
for (const line of lines) {
yield line;
}
}
// Only process first 100 lines, rest never evaluated
for (const line of readLines(hugeText)) {
if (processed >= 100) break;
processLine(line);
}That break statement is the key. The generator stops. No wasted work, no excess memory. Try doing that with a regular function that returns a full array.
yield* for Delegation
The yield* syntax delegates to another generator. I use this to compose pipelines ( like combining multiple data sources into one stream ).
function* users() {
yield { name: "Davide", city: "Montevideo" };
yield { name: "Ana", city: "Rome" };
}
function* products() {
yield { id: 1, price: 29 };
yield { id: 2, price: 49 };
}
function* allData() {
yield* users();
yield* products();
}
for (const item of allData()) {
console.log(item);
}Clean, readable, no nesting. Just streams feeding into streams.
Passing Values Back In
This is the part most people miss. .next(value) sends a value back into the generator. The yielded expression receives it.
function* askName() {
const name = yield "What is your name?";
yield `Hello, ${name}`;
}
const gen = askName();
gen.next(); // { value: "What is your name?", done: false }
gen.next("Davide"); // { value: "Hello, Davide", done: false }
gen.next(); // { value: undefined, done: true }I use this for state machines. Each yield is a state, and the input from .next() determines the transition. Way cleaner than a switch statement with 15 cases.
Async Generators ( the Real Power )
This is where generators finally click for most people. Combine function* with async/await and you can yield values from asynchronous sources.
async function* fetchPages(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
const res = await fetch(`${url}?page=${page}`);
const data = await res.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// Consume one page at a time
for await (const items of fetchPages("/api/products")) {
renderItems(items);
if (items.length === 0) break;
}That for await...of loop is the killer feature. No manual .next() calls, no callback hell. Just iterate over async results like they were a normal array.
What I Do Not Use Them For
Generators are not a replacement for everything. I do not use them for simple list transformations ( use .map()/.filter() ), I do not use them as coroutine libraries ( use async/await ), and I do not use them for event emitters ( use EventTarget ).
They are a specific tool for lazy, pauseable iteration. Use them for that and they are great. Use them for everything and your code becomes unreadable.
3 Rules I Follow
1. Always return an iterator protocol. Generators already implement Symbol.iterator, so they work with for...of and spread syntax out of the box.
2. Name them clearly. I prefix with the data source: readLines, fetchPages, walkTree. No generic names like process.
3. Always handle early termination. If someone breaks out of a for...of loop, the generator stays suspended. Add a try/finally if you need cleanup ( like closing a file handle ).
function* readWithCleanup(file) {
try {
while (file.hasMore()) {
yield file.readLine();
}
} finally {
file.close(); // runs even if consumer breaks early
}
}Generators are not complicated. The docs just make them look that way. Start with lazy iteration, add async when you need it, and ignore everything else until you actually have a use for it. :)