JavaScript Generators and Iterators

Understand the iterator protocol, write lazy sequences with generator functions, build custom iterables, and consume paginated APIs with async generators.

Advanced 12 min read 10 examples

The Iterator Protocol

Any object that implements a next() method returning { value, done } is an iterator. Any object with [Symbol.iterator]() that returns an iterator is an iterable.

JavaScript
// Manual iterator implementation
function makeCounter(start = 0, end = Infinity) {
    let current = start;
    return {
        next() {
            if (current <= end) {
                return { value: current++, done: false };
            }
            return { value: undefined, done: true };
        }
    };
}

const counter = makeCounter(1, 3);
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }

// Built-in iterators - arrays, strings, Maps, Sets are all iterable
const arr = [10, 20, 30];
const iter = arr[Symbol.iterator](); // get the iterator
console.log(iter.next()); // { value: 10, done: false }
console.log(iter.next()); // { value: 20, done: false }

// for...of uses the iterator protocol under the hood
for (const num of arr) {
    console.log(num); // 10, 20, 30
}

// Destructuring and spread also use the iterator protocol
const [first, ...rest] = arr; // first=10, rest=[20,30]
const copy = [...arr];         // [10, 20, 30]

Generator Basics

A generator function (function*) returns a generator object that is both an iterator and an iterable. It pauses at each yield.

JavaScript
// Generator function - note the * after function
function* simpleGenerator() {
    console.log('Before first yield');
    yield 1;
    console.log('Before second yield');
    yield 2;
    console.log('Before return');
    return 3; // done: true
}

const gen = simpleGenerator(); // does NOT run the function body yet

console.log(gen.next()); // logs "Before first yield"  -> { value: 1, done: false }
console.log(gen.next()); // logs "Before second yield" -> { value: 2, done: false }
console.log(gen.next()); // logs "Before return"       -> { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }

// Generator is also iterable - use with for...of, spread, destructuring
function* colors() {
    yield 'red';
    yield 'green';
    yield 'blue';
}

for (const color of colors()) {
    console.log(color); // red, green, blue
}
// for...of ignores the return value (done: true) - only gets yield values

const colorArray = [...colors()]; // ['red', 'green', 'blue']
const [first]    = colors();      // 'red'

yield and Return

JavaScript
// yield* - delegate to another iterable
function* inner() {
    yield 'a';
    yield 'b';
}
function* outer() {
    yield 1;
    yield* inner();  // yields 'a', then 'b' inline
    yield* [10, 20]; // yield* works with any iterable
    yield 2;
}
console.log([...outer()]); // [1, 'a', 'b', 10, 20, 2]

// Passing values INTO a generator via next(value)
function* calculator() {
    const a = yield 'Enter first number:';
    const b = yield 'Enter second number:';
    yield `Result: ${a + b}`;
}
const calc = calculator();
console.log(calc.next().value);   // 'Enter first number:'
console.log(calc.next(5).value);  // 'Enter second number:' (5 assigned to a)
console.log(calc.next(3).value);  // 'Result: 8' (3 assigned to b)

// generator.return(value) - force-close the generator
function* counter() {
    let i = 0;
    while (true) yield i++;
}
const c = counter();
c.next(); // { value: 0, done: false }
c.return('stopped'); // { value: 'stopped', done: true }
c.next(); // { value: undefined, done: true } - generator is closed

// generator.throw(error) - inject an error at the yield point
function* safe() {
    try {
        yield 1;
    } catch (e) {
        console.log('Caught:', e.message);
        yield 'recovered';
    }
}
const s = safe();
s.next();                          // { value: 1, done: false }
s.throw(new Error('oops'));        // logs 'Caught: oops' -> { value: 'recovered', done: false }

Infinite Sequences

Generators are perfect for infinite sequences because values are produced lazily - only when requested. The sequence never materializes in memory all at once.

JavaScript
// Infinite integer sequence
function* integers(start = 0, step = 1) {
    let n = start;
    while (true) {
        yield n;
        n += step;
    }
}

// Take first N values from an infinite generator
function take(gen, n) {
    const result = [];
    for (const value of gen) {
        result.push(value);
        if (result.length === n) break; // break closes the generator
    }
    return result;
}

console.log(take(integers(0, 2), 5));  // [0, 2, 4, 6, 8]
console.log(take(integers(10), 4));    // [10, 11, 12, 13]

// Fibonacci sequence
function* fibonacci() {
    let [a, b] = [0, 1];
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}
console.log(take(fibonacci(), 8)); // [0, 1, 1, 2, 3, 5, 8, 13]

// ID generator - useful for generating unique IDs
function* idGenerator(prefix = 'id') {
    let n = 1;
    while (true) yield `${prefix}-${n++}`;
}
const nextId = idGenerator('user');
console.log(nextId.next().value); // 'user-1'
console.log(nextId.next().value); // 'user-2'

Symbol.iterator

Adding [Symbol.iterator]() to any object makes it work with for...of, spread, and destructuring.

JavaScript
// Make a custom object iterable
class Range {
    constructor(start, end) {
        this.start = start;
        this.end   = end;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const end   = this.end;
        return {
            next() {
                if (current <= end) return { value: current++, done: false };
                return { value: undefined, done: true };
            }
        };
    }
}

const range = new Range(1, 5);
for (const n of range) console.log(n); // 1 2 3 4 5
console.log([...range]);               // [1, 2, 3, 4, 5]
const [a, b, ...rest] = range;         // a=1, b=2, rest=[3,4,5]

// Simpler: use a generator as [Symbol.iterator]
class Range2 {
    constructor(start, end) { this.start = start; this.end = end; }

    *[Symbol.iterator]() { // generator method shorthand
        for (let i = this.start; i <= this.end; i++) yield i;
    }
}

// Tree traversal with a generator
class TreeNode {
    constructor(value, left = null, right = null) {
        this.value = value;
        this.left  = left;
        this.right = right;
    }

    *[Symbol.iterator]() { // in-order traversal
        if (this.left)  yield* this.left;
        yield this.value;
        if (this.right) yield* this.right;
    }
}

const tree = new TreeNode(4,
    new TreeNode(2, new TreeNode(1), new TreeNode(3)),
    new TreeNode(6, new TreeNode(5), new TreeNode(7))
);
console.log([...tree]); // [1, 2, 3, 4, 5, 6, 7]

Practical Generator Patterns

JavaScript
// Pattern 1: Lazy map/filter for large datasets
function* lazyMap(iterable, fn) {
    for (const value of iterable) yield fn(value);
}
function* lazyFilter(iterable, pred) {
    for (const value of iterable) if (pred(value)) yield value;
}

const millionNumbers = integers(1);
const pipeline = lazyFilter(lazyMap(millionNumbers, x => x * x), x => x % 3 === 0);
console.log(take(pipeline, 5)); // [9, 36, 81, 144, 225] - computed lazily

// Pattern 2: Chunking - split an array into batches
function* chunks(array, size) {
    for (let i = 0; i < array.length; i += size) {
        yield array.slice(i, i + size);
    }
}
for (const batch of chunks([1,2,3,4,5,6,7], 3)) {
    console.log(batch); // [1,2,3], [4,5,6], [7]
}

// Pattern 3: Zip multiple iterables together
function* zip(...iterables) {
    const iters = iterables.map(it => it[Symbol.iterator]());
    while (true) {
        const results = iters.map(it => it.next());
        if (results.some(r => r.done)) return;
        yield results.map(r => r.value);
    }
}
const names   = ['Alice', 'Bob', 'Carol'];
const scores  = [95, 87, 91];
for (const [name, score] of zip(names, scores)) {
    console.log(`${name}: ${score}`); // Alice: 95, Bob: 87, Carol: 91
}

Async Generators

Combine async and function* to produce values asynchronously. Use for await...of to consume them.

JavaScript
// Async generator - can use await inside
async function* fetchPages(baseUrl) {
    let page = 1;
    while (true) {
        const res  = await fetch(`${baseUrl}?page=${page}`);
        const data = await res.json();
        if (data.items.length === 0) return; // no more pages
        yield data.items;
        if (!data.hasMore) return;
        page++;
    }
}

// Consume with for await...of
async function loadAllProducts() {
    const allProducts = [];
    for await (const page of fetchPages('/api/products')) {
        allProducts.push(...page);
        console.log(`Loaded ${allProducts.length} products`);
    }
    return allProducts;
}

// Async generator for real-time streaming (Server-Sent Events)
async function* readSSE(url) {
    const response = await fetch(url);
    const reader   = response.body.getReader();
    const decoder  = new TextDecoder();
    let buffer = '';
    while (true) {
        const { value, done } = await reader.read();
        if (done) return;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop(); // keep incomplete line
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                yield JSON.parse(line.slice(6));
            }
        }
    }
}

for await (const event of readSSE('/api/stream')) {
    console.log('Event:', event);
}

Frequently Asked Questions

An iterator is any object with a next() method that returns { value, done } objects. When done is false, value is the current item. When done is true, iteration is complete. Arrays, strings, Maps, Sets, and NodeLists are all iterables - objects with a [Symbol.iterator]() method that returns an iterator. This is what makes for...of work.

A generator function (function*) returns a generator object (which is both an iterator and an iterable). The magic is yield: the function pauses at each yield, returning the yielded value, and resumes from exactly that point on the next next() call. This makes lazy, on-demand value production possible without materializing the entire sequence in memory.

Common use cases: (1) lazy/infinite sequences where computing all values upfront is too expensive, (2) custom iteration over complex data structures (trees, graphs), (3) state machines where the generator function acts as a step-through logic, (4) async iteration over paginated APIs or streams with async generators. For most everyday iteration, Array.map/filter/reduce or for...of are simpler choices.

yield value yields a single value. yield* iterable delegates to another iterable and yields all its values one by one - like a nested loop inside the generator. You can yield* another generator, an array, a string, or any iterable. It also forwards the return value of the inner generator as the result of the yield* expression.