Generators
function* and yield — lazy sequences, custom iterators, two-way next(value), and how generators power async patterns.
- javascript
- generators
- iterators
- yield
A generator function (function*) returns a generator object — an iterator you can pause with yield and resume with .next(). They implement the iterable protocol for free and shine for lazy sequences, paginated streams, and custom iteration without allocating full arrays.
First contact
function* ids() {
let i = 1;
while (true) {
yield i++;
}
}
const gen = ids();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
// for...of pulls until done (infinite here — don't without break)
function* range(n) {
for (let i = 0; i < n; i++) yield i;
}
[...range(3)]; // [0, 1, 2]
Calling ids() does not run the body — it returns the generator. Execution starts on the first next().
Lazy pipelines
function* map(iter, fn) {
for (const x of iter) yield fn(x);
}
function* filter(iter, pred) {
for (const x of iter) if (pred(x)) yield x;
}
const result = [
...filter(
map(range(10), (x) => x * 2),
(x) => x % 3 === 0,
),
];
Nothing materializes until you consume with for...of, spread, or Array.from.
yield* and composition
function* a() {
yield 1;
yield 2;
}
function* b() {
yield* a();
yield 3;
}
[...b()]; // [1, 2, 3]
Two-way communication
next(value) sends a value into the generator as the result of the current yield:
function* talk() {
const name = yield 'who?';
yield `hi ${name}`;
}
const g = talk();
g.next(); // { value: 'who?', done: false }
g.next('Ada'); // { value: 'hi Ada', done: false }
g.next(); // { value: undefined, done: true }
gen.throw(err) injects an exception at the yield point. gen.return(v) terminates early and runs finally.
function* withCleanup() {
try {
yield 1;
yield 2;
} finally {
console.log('cleanup');
}
}
const h = withCleanup();
h.next();
h.return(); // logs cleanup
Infinite sequences (with care)
function* fibonacci() {
let a = 0;
let b = 1;
for (;;) {
yield a;
[a, b] = [b, a + b];
}
}
function take(n, iter) {
const out = [];
for (const x of iter) {
out.push(x);
if (out.length === n) break;
}
return out;
}
take(6, fibonacci()); // [0,1,1,2,3,5]
Async angle
Generators are sync by themselves. Async generators (async function* + for await) handle async streams — see async iteration. Historically, libraries (co, Redux-Saga) used generators to express async flows before async/await was universal.
Interview answer
“function* returns an iterator that pauses at yield. next() resumes; next(value) feeds the last yield. Generators are iterable, support yield* delegation, and enable lazy sequences. finally runs on return/throw. For async streams I use async function*.”
Related
Implementing iterables for UI data
function* walkDom(root) {
const stack = [root];
while (stack.length) {
const node = stack.pop();
yield node;
for (let i = node.childNodes.length - 1; i >= 0; i--) {
stack.push(node.childNodes[i]);
}
}
}
for (const n of walkDom(document.body)) {
if (n.nodeType === Node.TEXT_NODE && n.textContent.includes('TODO')) {
console.log(n);
}
}
Generators keep traversal state without building a giant array of nodes. Same idea for tree widgets and filesystem-like nested comments. Prefer async generators when each step awaits I/O.
Further reading
Related guides
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.
- Iterators and the Iterable ProtocolSymbol.iterator, next(), and for...of — how iterables work, custom iterators, and the difference from arrays.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.