What are generators in JavaScript?
Generators are JavaScript functions that pause mid-execution and resume exactly where they left off, producing values one at a time via the yield keyword.
Theory
TL;DR
- Think of a vending machine: you insert a coin (
.next()), get one item (yieldvalue), and the machine waits for the next coin instead of dispensing everything at once. - Regular functions run to completion and return once. Generators return an iterator object immediately and produce values lazily, one per
.next()call. - Use generators when processing large datasets or infinite sequences where loading everything into memory at once is not practical (typically 10k+ items).
function*andyieldare not optional syntax sugar. They are what make a function a generator.
Quick example
function* numberGen() {
yield 1; // pauses here on first .next()
yield 2;
yield 3;
return 4; // final value, then done: true
}
const gen = numberGen();
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: 4, done: true }Each .next() resumes from the last yield, returns { value, done }, and pauses again. After return, every subsequent .next() returns { value: undefined, done: true }.
Key difference from regular functions
A regular function runs to completion the moment you call it and returns one value. A generator returns an iterator object immediately, without running any body code. The body starts only on the first .next(), then pauses at each yield. This is lazy evaluation: values compute only when requested, not upfront.
When to use generators
- Large or infinite data: Generate paginated API results, number ranges, or tree traversals on demand instead of building a full array first.
- State machines: Track multi-step processes like game turns or form wizards without a class.
- Two-way communication: Pass values back into a running generator via
.next(value), used heavily in Redux-Saga. - Skip generators for simple one-shot computations. Regular functions are the right tool there.
How V8 compiles generators
V8 compiles function* into a state machine. Each yield becomes a state transition point. The generator object holds a pointer to a suspended execution context. When you call .next(arg), the engine resumes from that state, and arg becomes the return value of the yield expression inside the function. That is why you can send values back in, not just get them out.
Common mistakes
Expecting the generator to run on creation
function* gen() { yield 1; }
const g = gen(); // g is an iterator, NOT the value 1
console.log(g); // Generator {}No code runs until you call .next(). Creating the generator and running it are two separate steps.
Forgetting to check done: true
function* gen() { yield 1; }
const g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: undefined, done: true }
console.log(g.next().value); // undefined - generator is exhaustedAfter the generator returns, all subsequent .next() calls give { value: undefined, done: true }. Use a while loop to guard against this:
let result = g.next();
while (!result.done) {
console.log(result.value);
result = g.next();
}Accidentally sharing state across instances
let count = 0;
function* sharedGen() { yield count++; }
const g1 = sharedGen(); g1.next(); // { value: 0, done: false }
const g2 = sharedGen(); g2.next(); // { value: 1, done: false } - shared outer count!Each invocation shares the outer closure. Move state inside to get independent generators:
function* gen() { let count = 0; yield count++; }Misunderstanding value injection on the first .next()
Any argument you pass to the first .next() call is silently discarded. No yield expression inside the function is waiting to receive it yet. From the second call onwards, the argument to .next(arg) becomes the return value of the previous yield expression. The Advanced example below shows a full walkthrough.
Real-world usage
- Redux-Saga: Effects like
call()andtakeEvery()are built on generator pausing. The saga middleware calls.next()with resolved values to coordinate async operations. - Node.js streams:
readableStream[Symbol.asyncIterator]()uses async generators for backpressure handling (Node 10+). - Infinite sequences: ID generators, number ranges, or test data factories where you don't know upfront how many values you need.
- Paginated DB queries: Fetch in batches, yielding each page, without loading the full dataset into memory.
I used this pattern in a Node.js service processing 500k+ user records. Loading everything at once caused memory spikes. Switching to a generator that fetched batches of 500 kept memory flat for the entire run.
Follow-up questions
Q: How do you iterate over a generator?
A: Three ways: for...of (auto-calls .next() until done: true), spread operator ([...gen] converts to array), or a manual while (!result.done) loop. Use for...of unless you specifically need the result as an array.
Q: What is yield* and how does it differ from yield?
A: yield pauses with a single value. yield* delegates to another iterable or generator and yields all its values in sequence. It is shorthand for a loop that yields each item from the inner iterable one by one.
Q: Can a generator handle errors?
A: Yes. Call .throw(err) on the generator to inject an error at the current yield point. If the generator wraps that yield in try/catch, it handles the error and continues. If not, the error propagates to the caller.
Q: What is the memory benefit of generators over arrays for large sequences?
A: An array holding 1 million items allocates memory for all of them upfront. A generator produces one value per .next(), reusing the same execution context with no extra stack allocation. For infinite sequences, an array is simply not an option.
Q: How do generators connect to async iteration (senior-level)?
A: async function* combines generators with promises. Each yield can await an async operation before producing a value. The caller iterates with for await...of. V8 handles this via bytecode suspension, the same mechanism as regular generators but integrated with promise resolution.
Examples
Basic: range generator with for...of
The simplest real use case: generating a number range without building an array.
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // pauses after each value
}
}
for (const num of range(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
// for...of handles .next() and the done check automaticallyfor...of calls .next() internally and stops when done: true. No manual loop boilerplate needed.
Intermediate: paginated database fetch
Fetching users page by page, without loading all records into memory at once.
function* fetchUsersPaginated(pageSize = 5) {
let page = 0;
while (true) {
const users = fetchUsersFromDB(page, pageSize); // hypothetical DB call
if (users.length === 0) return; // stops when no more data
yield users; // one page at a time
page++;
}
}
const userGen = fetchUsersPaginated();
console.log(userGen.next().value); // [user1, user2, user3, user4, user5]
console.log(userGen.next().value); // [user6, user7, user8, user9, user10]
// only the current page is in memory at any given momentThe generator pauses after each yield users and only fetches the next page when you call .next() again. Memory usage stays predictable regardless of total record count.
Advanced: two-way communication via .next(value)
The pattern that trips up most developers. Values flow both out (via yield) and in (via .next(arg)).
function* auctionGen() {
const bid = yield 'Waiting for first bid'; // pauses; 'bid' gets the argument of the next .next()
const counterBid = yield `Current high: ${bid}`; // pauses again
yield `Sold for ${Math.max(bid, counterBid)}`;
}
const auction = auctionGen();
console.log(auction.next()); // { value: 'Waiting for first bid', done: false }
console.log(auction.next(100)); // 100 becomes 'bid' → { value: 'Current high: 100', done: false }
console.log(auction.next(150)); // 150 becomes 'counterBid' → { value: 'Sold for 150', done: false }Redux-Saga uses exactly this mechanism. The saga middleware sends resolved promise values back into the generator via .next(), letting sagas read async results as synchronous assignments.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.