Generator Function Syntax
A generator function is declared with function* (asterisk). Calling it does not execute the body β it returns a generator object. The body only runs when you call .next() on the generator object.
function* simpleGenerator() {
console.log('Body: before first yield');
yield 1;
console.log('Body: after first yield');
yield 2;
console.log('Body: after second yield');
yield 3;
console.log('Body: done');
}
const gen = simpleGenerator();
console.log('Generator created β body not running yet');
let result;
result = gen.next();
console.log('next() returned:', result); // { value: 1, done: false }
result = gen.next();
console.log('next() returned:', result); // { value: 2, done: false }
result = gen.next();
console.log('next() returned:', result); // { value: 3, done: false }
result = gen.next();
console.log('next() returned:', result); // { value: undefined, done: true }
Generator created β body not running yet
Body: before first yield
next() returned: { value: 1, done: false }
Body: after first yield
next() returned: { value: 2, done: false }
Body: after second yield
next() returned: { value: 3, done: false }
Body: done
next() returned: { value: undefined, done: true }
Infinite Sequence Generator
Generators can represent infinite sequences because values are computed lazily β only when requested. A while(true) loop that yields is perfectly safe inside a generator.
// Infinite integer counter
function* counter(start = 0, step = 1) {
let current = start;
while (true) {
yield current;
current += step;
}
}
// Fibonacci sequence (infinite)
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Helper: take first N values from any generator
function take(gen, n) {
const result = [];
for (const value of gen) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
console.log(take(counter(1, 2), 6)); // [1, 3, 5, 7, 9, 11]
console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
console.log(take(counter(100, -10), 5)); // [100, 90, 80, 70, 60]
[1, 3, 5, 7, 9, 11] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] [100, 90, 80, 70, 60]
Passing Values Into Generators via next(value)
You can pass a value into a running generator via gen.next(value). The passed value becomes the result of the yield expression inside the generator. This enables two-way communication.
function* dialog() {
const name = yield 'What is your name?';
const hobbies = yield `Nice to meet you, ${name}! What are your hobbies?`;
yield `Cool, ${name} β I enjoy ${hobbies} too!`;
}
const conv = dialog();
let q = conv.next(); // start: value = first question
console.log(q.value); // 'What is your name?'
q = conv.next('Alice'); // send 'Alice' as the result of first yield
console.log(q.value); // 'Nice to meet you, Alice! What are your hobbies?'
q = conv.next('coding'); // send 'coding' as result of second yield
console.log(q.value); // 'Cool, Alice β I enjoy coding too!'
What is your name? Nice to meet you, Alice! What are your hobbies? Cool, Alice β I enjoy coding too!
The Symbol.iterator Protocol
An object is iterable if it has a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a .next() method that returns { value, done }. Generators automatically implement both protocols.
// Custom iterable class using a generator
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
// The Symbol.iterator method makes instances iterable
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i += this.step) {
yield i;
}
}
// Convenience: collect to array
toArray() { return [...this]; }
}
const r = new Range(1, 10, 2);
// Works with for...of
for (const n of r) {
process.stdout.write(n + ' ');
}
console.log();
// Works with spread
console.log([...new Range(0, 6, 3)]);
// Works with destructuring
const [a, b, c] = new Range(10, 50, 15);
console.log(a, b, c);
1 3 5 7 9 [0, 3, 6] 10 25 40
yield* Delegation
yield* delegates to another iterable (another generator, array, string, etc.). It's like yielding every item from the inner iterable one by one.
function* inner() {
yield 'a';
yield 'b';
}
function* outer() {
yield 1;
yield* inner(); // delegate to inner generator
yield* [10, 20, 30]; // delegate to array
yield* 'hi'; // delegate to string (character by character)
yield 2;
}
console.log([...outer()]);
// [1, 'a', 'b', 10, 20, 30, 'h', 'i', 2]
[1, 'a', 'b', 10, 20, 30, 'h', 'i', 2]
Practical Use Cases
// Use-case 1: Unique ID factory
function* idFactory(prefix = 'id') {
let n = 1;
while (true) {
yield `${prefix}-${String(n++).padStart(4, '0')}`;
}
}
const userIds = idFactory('usr');
console.log(userIds.next().value); // usr-0001
console.log(userIds.next().value); // usr-0002
// Use-case 2: Paginated API fetching
async function* paginatedFetch(baseUrl) {
let page = 1;
while (true) {
const res = await fetch(`${baseUrl}?page=${page}&limit=10`);
const data = await res.json();
if (data.length === 0) break;
yield data; // yield entire page at once
page++;
}
}
// Consumer
async function fetchAllUsers() {
const allUsers = [];
for await (const page of paginatedFetch('/api/users')) {
allUsers.push(...page);
console.log(`Fetched ${allUsers.length} users so far`);
}
return allUsers;
}
// Use-case 3: Tree traversal
function* flattenTree(node) {
yield node.value;
if (node.children) {
for (const child of node.children) {
yield* flattenTree(child); // recursive delegation
}
}
}
const tree = { value: 1, children: [
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
{ value: 3, children: [{ value: 6 }] }
]};
console.log([...flattenTree(tree)]); // [1, 2, 4, 5, 3, 6]
usr-0001 usr-0002 [1, 2, 4, 5, 3, 6]
| Use Case | Why Generators Help | Alternative |
|---|---|---|
| Infinite sequences | Lazy β compute only when asked | Cannot do with plain arrays |
| ID factories | Stateful counter, trivially simple | Closure variable |
| Pagination | Async generators hide fetching logic | Recursive async functions |
| Tree traversal | yield* enables elegant recursion | Manual stack with array |
| Custom iteration | Symbol.iterator + generator = minimal code | Manual iterator object |
Prefix a generator function with both async and *: async function*. It can both await Promises and yield values. Consume it with for await...of. This is the standard pattern for streaming data sources like paginated APIs, file line readers, and WebSocket streams.
Once a generator is exhausted (done: true), calling .next() will always return { value: undefined, done: true }. To restart, create a new generator by calling the generator function again.
ποΈ Practical Exercise
Implement these generator utilities:
map*(gen, fn)β transforms each yielded value.filter*(gen, predicate)β yields only values that pass the predicate.zip*(gen1, gen2)β yields pairs[a, b]until either generator is done.- Chain them: filter even numbers from an infinite counter, map to squares, take first 5.
π₯ Challenge Exercise
Implement a generic pipeline(...generators) function that composes generator transformers left-to-right over an initial iterable. Then create a data processing pipeline that: reads a list of raw user objects β filters active users β maps to display names β takes the first 10 β collects to an array. All steps must be lazy generator functions.
Interview Questions
- What is a generator function and how is it different from a regular function?
- What does
yielddo? What does.next(value)do? - What is the iterator protocol (
{ value, done })? - How do you make a custom class iterable with Symbol.iterator?
- What is
yield*and when would you use it? - What is the difference between a generator and an async generator?
π Summary
- Generator functions (
function*) return a generator object; body runs only on.next(). yield exprpauses execution and sendsexprasvalueto the caller..next(value)resumes the generator;valuebecomes the result of theyieldexpression.- Generators automatically implement the iterator protocol and are usable with
for...ofand spread. - Add
[Symbol.iterator]*()to a class to make it iterable. yield*delegates to another iterable/generator.- Async generators (
async function*) combine await and yield for streaming async data.
Frequently Asked Questions
An iterable is any object with a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a .next() method that returns { value, done }. Generator objects are both β they have [Symbol.iterator]() (returns this) and .next().
Yes. return value inside a generator causes the generator to finish with { value: value, done: true }. Note that for...of ignores the done-true value β it only sees yielded values. The final return value is only visible if you call .next() manually after all yields are exhausted.
Call gen.throw(new Error('msg')). This resumes the generator as if the current yield threw the error. If the generator has a try/catch around the yield, it can handle the error and continue yielding. If not, the error propagates to the caller of .throw().