Generator functions
A generator function is a function that can pause its own execution and then continue from exactly the same place. It returns a generator object and hands out values one at a time through yield, which makes it a convenient base for iterators, lazy evaluation and asynchronous loops.
Theory
TL;DR
- The declaration looks like
function* name() {}: the asterisk afterfunctionmakes it a generator. - Calling it does not run the body, it returns a generator object (an iterator) with a
next()method. yieldhands out a value and freezes execution; the nextnext()resumes from the same line.- A generator follows the iterator protocol, so it works in
for...of, in spread and in destructuring. next(value)passes a value back into the generator, so the exchange goes both ways.async function*together withfor await...ofbrings the same thing to asynchronous streams (ES2018).
Quick example
function* gen() {
yield 1;
yield 2;
yield 3;
}
const iterator = gen();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }What happens here:
- the first
next()call runs up to the firstyield, returns1and freezes the function; - the following
next()resumes execution from the very same place; - after the last
yieldthe generator finishes its work and reportsdone: true.
Syntax and the generator object
A generator function is a function that returns a generator object (an iterator) and uses the yield keyword inside itself to pause execution and return values one by one.
function* name(args) {
// function body
}*afterfunctionturns the function into a generator;yieldis used to hand out values step by step;- when called, the function does not run immediately, it returns a generator (an iterator).
Every next() call returns an object shaped like { value, done }, where value is whatever yield produced and done tells you whether the generator has already finished.
Generators are iterators
A generator returns an object that is compatible with the iterator protocol. That means you can pass it straight into for...of:
function* numbers() {
yield 10;
yield 20;
yield 30;
}
for (const n of numbers()) {
console.log(n);
}Output:
10
20
30The same generator also works with the spread operator ([...numbers()]) and with array destructuring.
Lazy evaluation and infinite sequences
Generators can be infinite: they do not build the whole array up front, they produce values on demand. That is lazy evaluation, values are generated only as they are needed.
function* infiniteCounter() {
let i = 1;
while (true) {
yield i++;
}
}
const counter = infiniteCounter();
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// ...and so on foreverThe same approach is handy for finite sequences you would rather not materialise into an array:
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const num of range(1, 5)) {
console.log(num);
}Output:
1
2
3
4
5Two way exchange: next(value) and return
Data can travel not only out of the generator but back into it, through the argument of next(value). That argument becomes the result of the yield expression the generator is currently paused on.
function* dialog() {
const name = yield 'What is your name?';
yield `Hello, ${name}!`;
}
const it = dialog();
console.log(it.next().value); // "What is your name?"
console.log(it.next('Maria').value); // "Hello, Maria!"The first next() only starts the generator and runs it to the first yield, the second one passes 'Maria' inside as the result of that yield expression.
You can finish a generator early with return:
function* example() {
yield 1;
return 'end';
yield 2; // never runs
}
const it = example();
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 'end', done: true }Async generators and where all of this is used
Async generators (ES2018) let you use await inside iterable streams of data. You walk them with for await...of.
async function* fetchData() {
yield await Promise.resolve('First chunk');
yield await Promise.resolve('Second chunk');
}
for await (const part of fetchData()) {
console.log(part);
}What generators are actually good for:
| Use case | Example |
|---|---|
| Building iterators | Producing data element by element |
| Lazy evaluation | No need to keep the whole array in memory |
| Step by step execution | Controlling the flow of execution |
| Asynchronous pipelines | Processing data streams without loading memory |
| Emulating coroutines | The ability to stop and resume code |
A summary of the syntax and the capabilities:
| Property | Description |
|---|---|
| Definition | A function declared with function* that returns an iterator |
| Returns | A generator object |
| Keyword | yield |
| Can pause execution | Yes |
| Can take values back in | Yes |
| Asynchronous version | async function* |
| Main use | Lazy evaluation, iterators, asynchronous streams |
Common mistakes
- Thinking that calling a generator runs its body.
gen()only creates a generator object; not a single line of the body runs until you callnext(). - Confusing
yieldwithreturn.returnfinishes the generator: everything after it is never handed out, and the returned value arrives together withdone: true. - Expecting the first
next(value)to pass a value inside. The first call only runs the body up to the firstyield, so its argument is ignored. - Walking an infinite generator with
for...ofand nobreak. The loop will never end, you need an explicit limit. - Using
for...ofinstead offor await...ofwithasync function*. You will get promises back rather than the values themselves. - Reusing an exhausted generator. Once it reports
done: trueit does not restart: you have to call the generator function again and get a fresh object.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.