Iterators, Generators, and Iteration Protocols

1. Iterator Interface and Iterable Protocol

Iterator Protocol

Requirement Description Returns
next() method Returns object with value and done properties {value: any, done: boolean}
value Current iteration value Any type
done true if iterator finished, false otherwise Boolean

Iterable Protocol

Requirement Description Symbol
[Symbol.iterator] method Returns iterator object with next() method Symbol.iterator
Return value Object implementing iterator protocol Iterator with next()

Built-in Iterables

Type Iterable Iteration Order
Array ✓ Yes Index order (0, 1, 2, ...)
String ✓ Yes Character order
Map ✓ Yes Insertion order [key, value] pairs
Set ✓ Yes Insertion order values
TypedArray ✓ Yes Index order
arguments ✓ Yes Index order
NodeList ✓ Yes Index order
Object ✗ No (use Object.keys/values/entries) N/A

Consuming Iterables

Construct Syntax Description
for...of loop for (const x of iterable) Iterate over iterable values
Spread operator [...iterable] Convert iterable to array
Array.from() Array.from(iterable) Create array from iterable
Destructuring const [a, b] = iterable Extract values from iterable
yield* yield* iterable Delegate to iterable in generator
Promise.all() Promise.all(iterable) Wait for all promises in iterable
Map/Set constructor new Map(iterable) Create Map/Set from iterable

Example: Understanding iterator protocol

// Array is iterable
const arr = [1, 2, 3];

// Get iterator from iterable
const iterator = arr[Symbol.iterator]();

// Call next() manually
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}
console.log(iterator.next());  // {value: undefined, done: true}

// for...of uses iterator protocol internally
for (const value of arr) {
    console.log(value);  // 1, 2, 3
}

// String is iterable
const str = 'hello';
const strIterator = str[Symbol.iterator]();

console.log(strIterator.next());  // {value: 'h', done: false}
console.log(strIterator.next());  // {value: 'e', done: false}

for (const char of str) {
    console.log(char);  // 'h', 'e', 'l', 'l', 'o'
}

// Map is iterable (yields [key, value] pairs)
const map = new Map([['a', 1], ['b', 2]]);

for (const [key, value] of map) {
    console.log(key, value);  // 'a' 1, 'b' 2
}

// Set is iterable
const set = new Set([1, 2, 3]);

for (const value of set) {
    console.log(value);  // 1, 2, 3
}

Example: Creating simple iterator

// Simple iterator (not iterable)
function createRangeIterator(start, end) {
    let current = start;
    
    return {
        next() {
            if (current <= end) {
                return {
                    value: current++,
                    done: false
                };
            }
            return {
                value: undefined,
                done: true
            };
        }
    };
}

const rangeIter = createRangeIterator(1, 5);

console.log(rangeIter.next());  // {value: 1, done: false}
console.log(rangeIter.next());  // {value: 2, done: false}
console.log(rangeIter.next());  // {value: 3, done: false}
console.log(rangeIter.next());  // {value: 4, done: false}
console.log(rangeIter.next());  // {value: 5, done: false}
console.log(rangeIter.next());  // {value: undefined, done: true}

// Cannot use with for...of (not iterable, just iterator)
// for (const n of rangeIter) {}  // TypeError

Example: Creating iterable object

// Iterable object (has Symbol.iterator method)
const range = {
    start: 1,
    end: 5,
    
    // Implement iterable protocol
    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        
        // Return iterator
        return {
            next() {
                if (current <= end) {
                    return {
                        value: current++,
                        done: false
                    };
                }
                return {
                    value: undefined,
                    done: true
                };
            }
        };
    }
};

// Can use with for...of
for (const n of range) {
    console.log(n);  // 1, 2, 3, 4, 5
}

// Can use spread operator
const arr2 = [...range];
console.log(arr2);  // [1, 2, 3, 4, 5]

// Can destructure
const [first, second, ...rest] = range;
console.log(first);   // 1
console.log(second);  // 2
console.log(rest);    // [3, 4, 5]

// Each call to Symbol.iterator creates new iterator
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();

console.log(iter1.next());  // {value: 1, done: false}
console.log(iter2.next());  // {value: 1, done: false} (independent)

Example: Iterable with state

// Iterable collection
class LinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
        this.length = 0;
    }
    
    append(value) {
        const node = {value, next: null};
        
        if (!this.head) {
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node;
            this.tail = node;
        }
        
        this.length++;
    }
    
    // Make iterable
    [Symbol.iterator]() {
        let current = this.head;
        
        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {value, done: false};
                }
                return {value: undefined, done: true};
            }
        };
    }
}

const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);

// Use with for...of
for (const value of list) {
    console.log(value);  // 10, 20, 30
}

// Convert to array
const listArray = [...list];
console.log(listArray);  // [10, 20, 30]

// Use Array methods via spread
const doubled = [...list].map(x => x * 2);
console.log(doubled);  // [20, 40, 60]
Key Points: Iterator protocol: object with next() returning {value, done}. Iterable protocol: object with [Symbol.iterator] method returning iterator. Built-in iterables: Array, String, Map, Set, TypedArray. Iterables work with for...of, spread, destructuring. Each call to Symbol.iterator creates fresh iterator.

2. Generator Functions and yield Expressions

Generator Function Syntax

Type Syntax Example
Function Declaration function* name() {} function* gen() { yield 1; }
Function Expression const f = function*() {} const g = function*() { yield 1; }
Method *method() {} obj = {*gen() { yield 1; }}
Class Method *method() {} class C {*gen() { yield 1; }}
Arrow Function ❌ Not supported Cannot create generator arrows

yield Expression Types

Expression Syntax Description Returns
yield yield value Pause and yield value Value from next() or undefined
yield* yield* iterable Delegate to iterable/generator Final return value of iterable
yield (no value) yield Pause, yield undefined Value from next()
yield (assignment) const x = yield Receive value from next(value) Value passed to next()

Generator Object Properties

Feature Description Behavior
Is Iterable Has [Symbol.iterator]() method Returns itself
Is Iterator Has next() method Returns {value, done}
Lazy Executes on demand Code runs only when next() called
Pausable Execution pauses at yield Resumes on next next() call
State Maintains local state between yields Variables persist across pauses

Example: Basic generator functions

// Simple generator
function* simpleGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = simpleGenerator();

// Generator is both iterator and iterable
console.log(typeof gen.next);              // "function" (iterator)
console.log(typeof gen[Symbol.iterator]);  // "function" (iterable)

// Use as iterator
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: undefined, done: true}

// Use as iterable
function* gen2() {
    yield 1;
    yield 2;
    yield 3;
}

for (const value of gen2()) {
    console.log(value);  // 1, 2, 3
}

// Spread operator
const values = [...gen2()];
console.log(values);  // [1, 2, 3]

// Generator with logic
function* fibonacci(n) {
    let [a, b] = [0, 1];
    let count = 0;
    
    while (count < n) {
        yield a;
        [a, b] = [b, a + b];
        count++;
    }
}

console.log([...fibonacci(10)]);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Example: yield expressions and two-way communication

// Generator receiving values
function* twoWay() {
    console.log('Start');
    
    const a = yield 'First';
    console.log('Received:', a);
    
    const b = yield 'Second';
    console.log('Received:', b);
    
    return 'Done';
}

const gen3 = twoWay();

// First next() starts generator
console.log(gen3.next());        // Logs: "Start"
                                 // Returns: {value: 'First', done: false}

// Send value 10
console.log(gen3.next(10));      // Logs: "Received: 10"
                                 // Returns: {value: 'Second', done: false}

// Send value 20
console.log(gen3.next(20));      // Logs: "Received: 20"
                                 // Returns: {value: 'Done', done: true}

// Practical example: ID generator
function* idGenerator(prefix = 'ID') {
    let id = 1;
    
    while (true) {
        const reset = yield `${prefix}-${id}`;
        if (reset) {
            id = 1;
        } else {
            id++;
        }
    }
}

const ids = idGenerator('USER');
console.log(ids.next().value);       // "USER-1"
console.log(ids.next().value);       // "USER-2"
console.log(ids.next().value);       // "USER-3"
console.log(ids.next(true).value);   // "USER-1" (reset)
console.log(ids.next().value);       // "USER-2"

Example: yield* delegation

// Delegate to another generator
function* gen1() {
    yield 1;
    yield 2;
}

function* gen2() {
    yield 'a';
    yield* gen1();  // Delegate to gen1
    yield 'b';
}

console.log([...gen2()]);  // ['a', 1, 2, 'b']

// Delegate to any iterable
function* gen3() {
    yield* [1, 2, 3];
    yield* 'hello';
    yield* new Set([4, 5, 6]);
}

console.log([...gen3()]);
// [1, 2, 3, 'h', 'e', 'l', 'l', 'o', 4, 5, 6]

// Tree traversal
const tree = {
    value: 1,
    children: [
        {
            value: 2,
            children: [
                {value: 4, children: []},
                {value: 5, children: []}
            ]
        },
        {
            value: 3,
            children: [
                {value: 6, children: []}
            ]
        }
    ]
};

function* traverse(node) {
    yield node.value;
    for (const child of node.children) {
        yield* traverse(child);  // Recursive delegation
    }
}

console.log([...traverse(tree)]);  // [1, 2, 4, 5, 3, 6]

// Flatten nested arrays
function* flatten(arr) {
    for (const item of arr) {
        if (Array.isArray(item)) {
            yield* flatten(item);
        } else {
            yield item;
        }
    }
}

const nested = [1, [2, [3, 4], 5], 6];
console.log([...flatten(nested)]);  // [1, 2, 3, 4, 5, 6]

// yield* returns the final value
function* innerGen() {
    yield 1;
    yield 2;
    return 'finished';
}

function* outerGen() {
    const result = yield* innerGen();
    console.log('Inner returned:', result);  // "finished"
    yield 3;
}

console.log([...outerGen()]);
// Logs: "Inner returned: finished"
// Returns: [1, 2, 3] (return value not included in iteration)

Example: Infinite sequences and lazy evaluation

// Infinite sequence
function* naturals() {
    let n = 1;
    while (true) {
        yield n++;
    }
}

const nums = naturals();
console.log(nums.next().value);  // 1
console.log(nums.next().value);  // 2
console.log(nums.next().value);  // 3

// Take first N
function* take(n, iterable) {
    let count = 0;
    for (const value of iterable) {
        if (count++ >= n) break;
        yield value;
    }
}

console.log([...take(5, naturals())]);  // [1, 2, 3, 4, 5]

// Filter
function* filter(predicate, iterable) {
    for (const value of iterable) {
        if (predicate(value)) {
            yield value;
        }
    }
}

function* evens() {
    yield* filter(n => n % 2 === 0, naturals());
}

console.log([...take(5, evens())]);  // [2, 4, 6, 8, 10]

// Map
function* map(fn, iterable) {
    for (const value of iterable) {
        yield fn(value);
    }
}

function* squares() {
    yield* map(n => n ** 2, naturals());
}

console.log([...take(5, squares())]);  // [1, 4, 9, 16, 25]

// Compose transformations (lazy!)
const result = take(5, 
    map(n => n ** 2,
        filter(n => n % 2 === 0, naturals())
    )
);

console.log([...result]);  // [4, 16, 36, 64, 100]

// Random number generator
function* randomInRange(min, max) {
    while (true) {
        yield Math.floor(Math.random() * (max - min + 1)) + min;
    }
}

const dice = randomInRange(1, 6);
console.log([...take(10, dice)]);  // 10 random numbers 1-6
Key Points: Generators are both iterator and iterable. Execution pauses at yield, resumes at next next(). yield* delegates to another iterable. Generators enable lazy evaluation (compute on demand). Two-way communication: yield sends value out, next(value) sends value in. Perfect for infinite sequences and streaming data.

3. Generator Methods and Return Values

Generator Methods

Method Syntax Description Effect
next() gen.next(value) Resume execution, send value to yield Returns {value, done}
return() gen.return(value) Terminate generator, set return value Returns {value, done: true}
throw() gen.throw(error) Throw error at yield point Returns {value, done} or throws

Generator Return Behavior

Scenario Result done value
yield expression Normal iteration false Yielded value
return statement Generator completes true Returned value
Implicit return (end) Generator completes true undefined
gen.return(value) Forced completion true Provided value
gen.throw(error) Error thrown at yield Depends on handling Next yield or throws

finally Block Behavior

Method finally Executes Notes
return() ✓ Yes Cleanup code runs before completion
throw() ✓ Yes Runs before exception propagates
Normal completion ✓ Yes Runs when generator ends normally

Example: Generator return() method

// Early termination with return()
function* gen4() {
    try {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
    } finally {
        console.log('Cleanup');
    }
}

const g4 = gen4();

console.log(g4.next());      // {value: 1, done: false}
console.log(g4.next());      // {value: 2, done: false}
console.log(g4.return(99));  // Logs: "Cleanup"
                             // Returns: {value: 99, done: true}
console.log(g4.next());      // {value: undefined, done: true}

// return() without value
function* gen5() {
    yield 1;
    yield 2;
    yield 3;
}

const g5 = gen5();

console.log(g5.next());    // {value: 1, done: false}
console.log(g5.return());  // {value: undefined, done: true}
console.log(g5.next());    // {value: undefined, done: true}

// Using return in for...of
function* gen6() {
    yield 1;
    yield 2;
    return 3;  // Return value not included in iteration
    yield 4;   // Never reached
}

const values = [...gen6()];
console.log(values);  // [1, 2] (return value excluded)

// But visible with manual next()
const g6 = gen6();
console.log(g6.next());  // {value: 1, done: false}
console.log(g6.next());  // {value: 2, done: false}
console.log(g6.next());  // {value: 3, done: true} (return value visible)

Example: Generator throw() method

// Error handling with throw()
function* gen7() {
    try {
        yield 1;
        yield 2;
        yield 3;
    } catch (e) {
        console.log('Caught:', e.message);
        yield 'error handled';
    }
    yield 4;
}

const g7 = gen7();

console.log(g7.next());                   // {value: 1, done: false}
console.log(g7.throw(new Error('Oops'))); // Logs: "Caught: Oops"
                                          // Returns: {value: 'error handled', done: false}
console.log(g7.next());                   // {value: 4, done: false}
console.log(g7.next());                   // {value: undefined, done: true}

// Uncaught error
function* gen8() {
    yield 1;
    yield 2;  // No try-catch
    yield 3;
}

const g8 = gen8();

console.log(g8.next());  // {value: 1, done: false}

try {
    g8.throw(new Error('Uncaught'));
} catch (e) {
    console.log('Exception propagated:', e.message);
    // "Exception propagated: Uncaught"
}

console.log(g8.next());  // {value: undefined, done: true} (generator closed)

// finally with throw
function* gen9() {
    try {
        yield 1;
        yield 2;
    } finally {
        console.log('Cleanup after error');
    }
}

const g9 = gen9();
console.log(g9.next());  // {value: 1, done: false}

try {
    g9.throw(new Error('Error'));
} catch (e) {
    console.log('Caught outside');
    // Logs: "Cleanup after error" (finally runs)
    // Logs: "Caught outside"
}

Example: Cleanup with finally

// Resource management
function* databaseQuery() {
    console.log('Opening connection');
    
    try {
        yield 'row 1';
        yield 'row 2';
        yield 'row 3';
    } finally {
        console.log('Closing connection');
    }
}

// Normal completion
console.log('--- Normal completion ---');
for (const row of databaseQuery()) {
    console.log(row);
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "row 3"
// "Closing connection"

// Early termination
console.log('--- Early termination ---');
const query = databaseQuery();
console.log(query.next().value);  // "Opening connection", "row 1"
console.log(query.next().value);  // "row 2"
query.return();                   // "Closing connection"

// Break in for...of
console.log('--- Break in loop ---');
for (const row of databaseQuery()) {
    console.log(row);
    if (row === 'row 2') break;  // Triggers cleanup
}
// Logs:
// "Opening connection"
// "row 1"
// "row 2"
// "Closing connection"

// Error handling
console.log('--- Error ---');
function* withCleanup() {
    console.log('Setup');
    
    try {
        yield 1;
        throw new Error('Internal error');
        yield 2;  // Never reached
    } catch (e) {
        console.log('Error in generator:', e.message);
    } finally {
        console.log('Cleanup');
    }
}

for (const value of withCleanup()) {
    console.log(value);
}
// Logs:
// "Setup"
// "1"
// "Error in generator: Internal error"
// "Cleanup"

Example: Return values and yield*

// Return value from generator
function* inner() {
    yield 1;
    yield 2;
    return 'inner done';
}

function* outer1() {
    const result = yield* inner();
    console.log('Inner returned:', result);
    yield 3;
}

console.log([...outer1()]);
// Logs: "Inner returned: inner done"
// Returns: [1, 2, 3]

// Return value not included in iteration
function* gen10() {
    yield 1;
    yield 2;
    return 42;
}

console.log([...gen10()]);  // [1, 2] (42 not included)

// But captured by yield*
function* outer2() {
    const returnValue = yield* gen10();
    console.log('Got return value:', returnValue);
    yield returnValue;
}

console.log([...outer2()]);
// Logs: "Got return value: 42"
// Returns: [1, 2, 42]

// Chaining generators
function* pipeline() {
    const r1 = yield* stage1();
    console.log('Stage 1 result:', r1);
    
    const r2 = yield* stage2(r1);
    console.log('Stage 2 result:', r2);
    
    return r2;
}

function* stage1() {
    yield 'processing...';
    return 'stage1-complete';
}

function* stage2(input) {
    yield `using ${input}`;
    return 'stage2-complete';
}

console.log([...pipeline()]);
// Logs: "Stage 1 result: stage1-complete"
// Logs: "Stage 2 result: stage2-complete"
// Returns: ["processing...", "using stage1-complete"]

// Final result
const gen11 = pipeline();
let result;
while (true) {
    result = gen11.next();
    if (result.done) {
        console.log('Final return:', result.value);  // "stage2-complete"
        break;
    }
}
Important: return() terminates generator and runs finally blocks. throw() throws error at yield point (must be caught or generator closes). Return values from generators not included in iteration but captured by yield*. Use finally for cleanup. Early break in for...of triggers cleanup via return().

4. Async Generators and for-await-of Loops

Async Generator Syntax

Type Syntax Returns
Function Declaration async function* name() {} AsyncGenerator object
Function Expression const f = async function*() {} AsyncGenerator object
Method async *method() {} AsyncGenerator object
Class Method async *method() {} AsyncGenerator object

AsyncGenerator Methods

Method Syntax Description Returns
next() await gen.next(value) Resume, send value Promise<{value, done}>
return() await gen.return(value) Terminate generator Promise<{value, done: true}>
throw() await gen.throw(error) Throw error at yield Promise<{value, done}>

for-await-of Loop

Feature Syntax Works With
Basic for await (const x of asyncIterable) Async iterables
Await promises Automatically awaits each value AsyncGenerator, async iterables
Context Must be in async function Any async function or top-level
Break/Continue Supported Same as regular for...of

Async Iterable Protocol

Requirement Description Returns
[Symbol.asyncIterator] Method returning async iterator Object with next() returning Promise
next() method Returns promise of {value, done} Promise<{value: any, done: boolean}>

Example: Basic async generators

// Simple async generator
async function* asyncNumbers() {
    yield 1;
    yield 2;
    yield 3;
}

// Consume with for-await-of
async function main1() {
    for await (const num of asyncNumbers()) {
        console.log(num);  // 1, 2, 3
    }
}

main1();

// Async generator with delays
async function* delayedSequence() {
    yield await Promise.resolve(1);
    await new Promise(resolve => setTimeout(resolve, 100));
    yield 2;
    await new Promise(resolve => setTimeout(resolve, 100));
    yield 3;
}

async function main2() {
    console.log('Start');
    
    for await (const value of delayedSequence()) {
        console.log(value);  // 1 (then 100ms), 2 (then 100ms), 3
    }
    
    console.log('Done');
}

main2();

// Fetch data in chunks
async function* fetchPages(baseUrl, maxPages) {
    for (let page = 1; page <= maxPages; page++) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();
        yield data;
    }
}

async function processAllPages() {
    for await (const pageData of fetchPages('/api/items', 5)) {
        console.log('Processing page:', pageData);
        // Process page data
    }
}

Example: Async generator methods

// Manual iteration
async function* asyncGen() {
    yield 1;
    yield 2;
    yield 3;
}

async function manual() {
    const gen = asyncGen();
    
    console.log(await gen.next());  // {value: 1, done: false}
    console.log(await gen.next());  // {value: 2, done: false}
    console.log(await gen.next());  // {value: 3, done: false}
    console.log(await gen.next());  // {value: undefined, done: true}
}

manual();

// Early termination with return()
async function* withCleanup() {
    try {
        yield 1;
        yield 2;
        yield 3;
    } finally {
        console.log('Cleanup');
    }
}

async function earlyExit() {
    const gen = withCleanup();
    
    console.log(await gen.next());      // {value: 1, done: false}
    console.log(await gen.return(99));  // Logs: "Cleanup"
                                        // Returns: {value: 99, done: true}
    console.log(await gen.next());      // {value: undefined, done: true}
}

earlyExit();

// Error handling with throw()
async function* withErrorHandling() {
    try {
        yield 1;
        yield 2;
        yield 3;
    } catch (e) {
        console.log('Caught:', e.message);
        yield 'recovered';
    }
}

async function throwError() {
    const gen = withErrorHandling();
    
    console.log(await gen.next());                   // {value: 1, done: false}
    console.log(await gen.throw(new Error('Oops'))); // Logs: "Caught: Oops"
                                                     // Returns: {value: 'recovered', done: false}
    console.log(await gen.next());                   // {value: undefined, done: true}
}

throwError();

Example: Streaming data processing

// Read file in chunks
async function* readFileInChunks(filename, chunkSize = 1024) {
    // Simulated file reading (in real code, use fs.createReadStream)
    const fileContent = 'Large file content...';
    
    for (let i = 0; i < fileContent.length; i += chunkSize) {
        const chunk = fileContent.slice(i, i + chunkSize);
        yield chunk;
        // Simulate async I/O
        await new Promise(resolve => setTimeout(resolve, 10));
    }
}

async function processFile() {
    for await (const chunk of readFileInChunks('data.txt', 100)) {
        console.log('Processing chunk:', chunk.length, 'bytes');
    }
}

// Event stream
async function* eventStream(eventEmitter, eventName) {
    const queue = [];
    let resolveNext;
    
    const handler = (data) => {
        if (resolveNext) {
            resolveNext(data);
            resolveNext = null;
        } else {
            queue.push(data);
        }
    };
    
    eventEmitter.on(eventName, handler);
    
    try {
        while (true) {
            if (queue.length > 0) {
                yield queue.shift();
            } else {
                yield await new Promise(resolve => {
                    resolveNext = resolve;
                });
            }
        }
    } finally {
        eventEmitter.off(eventName, handler);
    }
}

// Usage with EventEmitter
async function processEvents(emitter) {
    for await (const event of eventStream(emitter, 'data')) {
        console.log('Event:', event);
        if (event.type === 'end') break;
    }
}

// WebSocket stream
async function* websocketStream(url) {
    const ws = new WebSocket(url);
    const queue = [];
    let resolveNext;
    let done = false;
    
    ws.onmessage = (event) => {
        if (resolveNext) {
            resolveNext({value: event.data, done: false});
            resolveNext = null;
        } else {
            queue.push(event.data);
        }
    };
    
    ws.onclose = () => {
        done = true;
        if (resolveNext) {
            resolveNext({done: true});
        }
    };
    
    try {
        while (!done) {
            if (queue.length > 0) {
                yield queue.shift();
            } else {
                const result = await new Promise(resolve => {
                    resolveNext = resolve;
                });
                if (result.done) break;
                yield result.value;
            }
        }
    } finally {
        ws.close();
    }
}

async function processWebSocketMessages() {
    for await (const message of websocketStream('ws://example.com')) {
        console.log('Message:', message);
    }
}

Example: Combining async generators

// Merge multiple async streams
async function* merge(...asyncIterables) {
    const iterators = asyncIterables.map(it => it[Symbol.asyncIterator]());
    const promises = new Map(
        iterators.map((it, i) => [i, it.next().then(result => ({i, result}))])
    );
    
    while (promises.size > 0) {
        const {i, result} = await Promise.race(promises.values());
        
        if (result.done) {
            promises.delete(i);
        } else {
            yield result.value;
            promises.set(i, 
                iterators[i].next().then(result => ({i, result}))
            );
        }
    }
}

async function* source1() {
    yield 1;
    await new Promise(r => setTimeout(r, 100));
    yield 2;
}

async function* source2() {
    yield 'a';
    await new Promise(r => setTimeout(r, 50));
    yield 'b';
}

async function testMerge() {
    for await (const value of merge(source1(), source2())) {
        console.log(value);  // Order depends on timing: a, 1, b, 2 or a, b, 1, 2
    }
}

// Transform async stream
async function* map(fn, asyncIterable) {
    for await (const value of asyncIterable) {
        yield fn(value);
    }
}

async function* filter(predicate, asyncIterable) {
    for await (const value of asyncIterable) {
        if (predicate(value)) {
            yield value;
        }
    }
}

async function* numbers() {
    for (let i = 1; i <= 10; i++) {
        yield i;
        await new Promise(r => setTimeout(r, 10));
    }
}

async function processNumbers() {
    const evens = filter(n => n % 2 === 0, numbers());
    const doubled = map(n => n * 2, evens);
    
    for await (const value of doubled) {
        console.log(value);  // 4, 8, 12, 16, 20
    }
}

// Throttle async stream
async function* throttle(asyncIterable, delay) {
    for await (const value of asyncIterable) {
        yield value;
        await new Promise(resolve => setTimeout(resolve, delay));
    }
}

async function throttleExample() {
    for await (const num of throttle(numbers(), 100)) {
        console.log(num);  // 1, (100ms), 2, (100ms), 3, ...
    }
}
Key Points: async function* creates async generator. Use for await...of to iterate (must be in async context). Methods return promises: await gen.next(). Perfect for streaming data, event processing, paginated APIs. AsyncGenerator is both async iterable and async iterator.

5. Custom Iterator Implementation

Iterator Implementation Checklist

Step Requirement Implementation
1. Make iterable Add [Symbol.iterator] method Returns iterator object
2. Return iterator Iterator has next() method Returns {value, done}
3. Track state Maintain iteration position Use closure or object properties
4. Signal completion Set done: true when finished Return {value: undefined, done: true}
5. Optional: return/throw Add cleanup support Implement return() and throw() methods

Iterator Pattern Variations

Pattern When to Use Example
Simple Iterator Single iteration, no cleanup Range, sequence
Reusable Iterator Multiple iterations from same object Array, collection
Stateful Iterator Complex state management Tree traversal, pagination
Iterator with Cleanup Resource management needed File reader, DB cursor
Infinite Iterator Unbounded sequences Counter, random generator

Example: Custom collection iterators

// Simple range iterator
class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }
    
    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        const step = this.step;
        
        return {
            next() {
                if (current <= end) {
                    const value = current;
                    current += step;
                    return {value, done: false};
                }
                return {value: undefined, done: true};
            }
        };
    }
}

const range = new Range(1, 10, 2);

for (const n of range) {
    console.log(n);  // 1, 3, 5, 7, 9
}

console.log([...range]);  // [1, 3, 5, 7, 9]

// Multiple independent iterations
const iter1 = range[Symbol.iterator]();
const iter2 = range[Symbol.iterator]();

console.log(iter1.next().value);  // 1
console.log(iter2.next().value);  // 1 (independent)
console.log(iter1.next().value);  // 3
console.log(iter2.next().value);  // 3

// Linked list iterator
class Node {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }
    
    append(value) {
        const node = new Node(value);
        
        if (!this.head) {
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node;
            this.tail = node;
        }
        
        this.size++;
    }
    
    [Symbol.iterator]() {
        let current = this.head;
        
        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {value, done: false};
                }
                return {value: undefined, done: true};
            }
        };
    }
}

const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);

for (const value of list) {
    console.log(value);  // 10, 20, 30
}

// Map and filter on custom iterable
const doubled = [...list].map(x => x * 2);
console.log(doubled);  // [20, 40, 60]

Example: Iterators with cleanup

// Iterator with return() method
class FileReader {
    constructor(filename) {
        this.filename = filename;
    }
    
    [Symbol.iterator]() {
        let file = this.openFile(this.filename);
        let lineNumber = 0;
        
        return {
            next() {
                const line = this.readLine(file);
                
                if (line !== null) {
                    return {
                        value: {lineNumber: ++lineNumber, text: line},
                        done: false
                    };
                }
                
                this.closeFile(file);
                return {value: undefined, done: true};
            }.bind(this),
            
            return(value) {
                // Cleanup on early termination
                console.log('Closing file early');
                this.closeFile(file);
                return {value, done: true};
            }.bind(this)
        };
    }
    
    openFile(filename) {
        console.log('Opening file:', filename);
        return {/* file handle */};
    }
    
    readLine(file) {
        // Simulated
        return null;
    }
    
    closeFile(file) {
        console.log('Closing file');
    }
}

const reader = new FileReader('data.txt');

// Early break triggers return()
for (const line of reader) {
    console.log(line);
    if (line.lineNumber === 5) break;  // Calls return()
}
// Logs: "Closing file early"

// Iterator with throw() support
class SafeIterator {
    constructor(items) {
        this.items = items;
    }
    
    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;
        let errorHandled = false;
        
        return {
            next() {
                if (errorHandled) {
                    return {value: undefined, done: true};
                }
                
                if (index < items.length) {
                    return {value: items[index++], done: false};
                }
                
                return {value: undefined, done: true};
            },
            
            throw(error) {
                console.log('Error in iterator:', error.message);
                errorHandled = true;
                return {value: 'error-handled', done: false};
            },
            
            return(value) {
                console.log('Cleanup');
                return {value, done: true};
            }
        };
    }
}

const safe = new SafeIterator([1, 2, 3]);
const safeIter = safe[Symbol.iterator]();

console.log(safeIter.next());                   // {value: 1, done: false}
console.log(safeIter.throw(new Error('Oops'))); // Logs: "Error in iterator: Oops"
                                                // Returns: {value: 'error-handled', done: false}
console.log(safeIter.next());                   // {value: undefined, done: true}

Example: Advanced iterator patterns

// Bidirectional iterator
class BidirectionalList {
    constructor(items) {
        this.items = items;
    }
    
    [Symbol.iterator]() {
        let index = 0;
        const items = this.items;
        
        return {
            next() {
                if (index < items.length) {
                    return {value: items[index++], done: false};
                }
                return {value: undefined, done: true};
            },
            
            // Custom method for backward iteration
            prev() {
                if (index > 0) {
                    return {value: items[--index], done: false};
                }
                return {value: undefined, done: true};
            }
        };
    }
}

const biList = new BidirectionalList([1, 2, 3, 4, 5]);
const biIter = biList[Symbol.iterator]();

console.log(biIter.next().value);  // 1
console.log(biIter.next().value);  // 2
console.log(biIter.next().value);  // 3
console.log(biIter.prev().value);  // 2
console.log(biIter.prev().value);  // 1
console.log(biIter.next().value);  // 2

// Peekable iterator
class PeekableIterator {
    constructor(iterable) {
        this.iterator = iterable[Symbol.iterator]();
        this.peeked = null;
        this.hasPeeked = false;
    }
    
    next() {
        if (this.hasPeeked) {
            this.hasPeeked = false;
            return this.peeked;
        }
        return this.iterator.next();
    }
    
    peek() {
        if (!this.hasPeeked) {
            this.peeked = this.iterator.next();
            this.hasPeeked = true;
        }
        return this.peeked;
    }
    
    [Symbol.iterator]() {
        return this;
    }
}

const peekable = new PeekableIterator([1, 2, 3, 4, 5]);

console.log(peekable.peek().value);  // 1 (doesn't advance)
console.log(peekable.peek().value);  // 1 (still doesn't advance)
console.log(peekable.next().value);  // 1 (now advances)
console.log(peekable.next().value);  // 2
console.log(peekable.peek().value);  // 3
console.log(peekable.next().value);  // 3

// Cycling iterator
class CycleIterator {
    constructor(iterable) {
        this.iterable = iterable;
        this.iterator = null;
    }
    
    [Symbol.iterator]() {
        return this;
    }
    
    next() {
        if (!this.iterator) {
            this.iterator = this.iterable[Symbol.iterator]();
        }
        
        const result = this.iterator.next();
        
        if (result.done) {
            // Restart
            this.iterator = this.iterable[Symbol.iterator]();
            return this.iterator.next();
        }
        
        return result;
    }
}

const cycle = new CycleIterator([1, 2, 3]);

console.log(cycle.next().value);  // 1
console.log(cycle.next().value);  // 2
console.log(cycle.next().value);  // 3
console.log(cycle.next().value);  // 1 (cycles back)
console.log(cycle.next().value);  // 2
console.log(cycle.next().value);  // 3
console.log(cycle.next().value);  // 1 (cycles again)
Key Points: Implement [Symbol.iterator]() returning object with next(). Track state with closure or properties. Optional: return() for cleanup, throw() for error handling. Each [Symbol.iterator]() call should return new independent iterator. Use generators for simpler implementation.

6. Iterator Helper Methods (take, drop, filter)

Iterator Helper Methods (ES2024+) New

Method Syntax Description Returns
take() iter.take(n) Take first n elements Iterator
drop() iter.drop(n) Skip first n elements Iterator
filter() iter.filter(predicate) Filter elements by predicate Iterator
map() iter.map(fn) Transform each element Iterator
flatMap() iter.flatMap(fn) Map and flatten one level Iterator
reduce() iter.reduce(fn, init) Reduce to single value Any
forEach() iter.forEach(fn) Execute function for each element undefined
some() iter.some(predicate) Test if any element matches Boolean
every() iter.every(predicate) Test if all elements match Boolean
find() iter.find(predicate) Find first matching element Any or undefined
toArray() iter.toArray() Convert to array Array

Lazy vs Eager Evaluation

Category Methods Evaluation Chainable
Lazy take, drop, filter, map, flatMap On demand, returns iterator ✓ Yes
Eager reduce, forEach, some, every, find, toArray Immediate, returns value ✗ No (terminal)

Example: Polyfill implementations (pre-ES2024)

// Helper method polyfills for older environments
function* take(n, iterable) {
    let count = 0;
    for (const value of iterable) {
        if (count++ >= n) break;
        yield value;
    }
}

function* drop(n, iterable) {
    let count = 0;
    for (const value of iterable) {
        if (count++ >= n) {
            yield value;
        }
    }
}

function* filter(predicate, iterable) {
    for (const value of iterable) {
        if (predicate(value)) {
            yield value;
        }
    }
}

function* map(fn, iterable) {
    for (const value of iterable) {
        yield fn(value);
    }
}

function* flatMap(fn, iterable) {
    for (const value of iterable) {
        yield* fn(value);
    }
}

// Usage examples
function* naturals() {
    let n = 1;
    while (true) yield n++;
}

// Take first 5 numbers
const first5 = [...take(5, naturals())];
console.log(first5);  // [1, 2, 3, 4, 5]

// Skip first 10, take next 5
const numbers = [...take(5, drop(10, naturals()))];
console.log(numbers);  // [11, 12, 13, 14, 15]

// Filter even numbers
const evens = filter(n => n % 2 === 0, naturals());
console.log([...take(5, evens)]);  // [2, 4, 6, 8, 10]

// Chain transformations (lazy evaluation)
const result = take(5,
    map(n => n * n,
        filter(n => n % 2 === 0, naturals())
    )
);

console.log([...result]);  // [4, 16, 36, 64, 100]

// FlatMap example
function* duplicateEach(n) {
    yield n;
    yield n;
}

const duplicated = flatMap(duplicateEach, [1, 2, 3]);
console.log([...duplicated]);  // [1, 1, 2, 2, 3, 3]

Example: Terminal operations

// Reduce
function reduce(fn, initial, iterable) {
    let accumulator = initial;
    
    for (const value of iterable) {
        accumulator = fn(accumulator, value);
    }
    
    return accumulator;
}

function* range(start, end) {
    for (let i = start; i <= end; i++) {
        yield i;
    }
}

const sum = reduce((acc, n) => acc + n, 0, range(1, 10));
console.log(sum);  // 55

// forEach (side effects)
function forEach(fn, iterable) {
    for (const value of iterable) {
        fn(value);
    }
}

forEach(n => console.log(n), range(1, 5));
// Logs: 1, 2, 3, 4, 5

// some
function some(predicate, iterable) {
    for (const value of iterable) {
        if (predicate(value)) {
            return true;
        }
    }
    return false;
}

console.log(some(n => n > 5, range(1, 10)));  // true
console.log(some(n => n > 20, range(1, 10))); // false

// every
function every(predicate, iterable) {
    for (const value of iterable) {
        if (!predicate(value)) {
            return false;
        }
    }
    return true;
}

console.log(every(n => n > 0, range(1, 10)));  // true
console.log(every(n => n < 5, range(1, 10)));  // false

// find
function find(predicate, iterable) {
    for (const value of iterable) {
        if (predicate(value)) {
            return value;
        }
    }
    return undefined;
}

console.log(find(n => n > 5, range(1, 10)));  // 6
console.log(find(n => n > 20, range(1, 10))); // undefined

// toArray
function toArray(iterable) {
    return [...iterable];
}

console.log(toArray(range(1, 5)));  // [1, 2, 3, 4, 5]

Example: Practical use cases

// Process large dataset lazily
function* readLargeFile() {
    // Simulate reading millions of lines
    for (let i = 1; i <= 1000000; i++) {
        yield `Line ${i}`;
    }
}

// Only processes first 100 matching lines (lazy!)
const validLines = take(100,
    filter(line => line.includes('error'),
        map(line => line.toLowerCase(),
            readLargeFile()
        )
    )
);

// No processing until we iterate
for (const line of validLines) {
    console.log(line);
}

// Pagination helper
function* paginate(items, pageSize) {
    for (let i = 0; i < items.length; i += pageSize) {
        yield items.slice(i, i + pageSize);
    }
}

const data = [...range(1, 100)];
const pages = paginate(data, 10);

// Get specific page
const page3 = [...take(1, drop(2, pages))][0];
console.log(page3);  // [21, 22, 23, ..., 30]

// Sliding window
function* window(size, iterable) {
    const buffer = [];
    
    for (const value of iterable) {
        buffer.push(value);
        
        if (buffer.length === size) {
            yield [...buffer];
            buffer.shift();
        }
    }
}

const windows = [...window(3, [1, 2, 3, 4, 5])];
console.log(windows);
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

// Batch processing
function* batch(size, iterable) {
    let batch = [];
    
    for (const value of iterable) {
        batch.push(value);
        
        if (batch.length === size) {
            yield batch;
            batch = [];
        }
    }
    
    if (batch.length > 0) {
        yield batch;
    }
}

const batches = [...batch(3, range(1, 10))];
console.log(batches);
// [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

// Zip iterators
function* zip(...iterables) {
    const iterators = iterables.map(it => it[Symbol.iterator]());
    
    while (true) {
        const results = iterators.map(it => it.next());
        
        if (results.some(r => r.done)) {
            break;
        }
        
        yield results.map(r => r.value);
    }
}

const zipped = [...zip([1, 2, 3], ['a', 'b', 'c'], [true, false, true])];
console.log(zipped);
// [[1, 'a', true], [2, 'b', false], [3, 'c', true]]

// Enumerate (with index)
function* enumerate(iterable) {
    let index = 0;
    for (const value of iterable) {
        yield [index++, value];
    }
}

const enumerated = [...enumerate(['a', 'b', 'c'])];
console.log(enumerated);
// [[0, 'a'], [1, 'b'], [2, 'c']]

Example: Composing iterator helpers

// Create reusable iterator pipelines
class IteratorPipeline {
    constructor(iterable) {
        this.iterable = iterable;
    }
    
    take(n) {
        this.iterable = take(n, this.iterable);
        return this;
    }
    
    drop(n) {
        this.iterable = drop(n, this.iterable);
        return this;
    }
    
    filter(predicate) {
        this.iterable = filter(predicate, this.iterable);
        return this;
    }
    
    map(fn) {
        this.iterable = map(fn, this.iterable);
        return this;
    }
    
    flatMap(fn) {
        this.iterable = flatMap(fn, this.iterable);
        return this;
    }
    
    // Terminal operations
    toArray() {
        return [...this.iterable];
    }
    
    reduce(fn, initial) {
        return reduce(fn, initial, this.iterable);
    }
    
    forEach(fn) {
        forEach(fn, this.iterable);
    }
    
    some(predicate) {
        return some(predicate, this.iterable);
    }
    
    every(predicate) {
        return every(predicate, this.iterable);
    }
    
    find(predicate) {
        return find(predicate, this.iterable);
    }
}

// Helper function
function pipeline(iterable) {
    return new IteratorPipeline(iterable);
}

// Usage
const result1 = pipeline(naturals())
    .filter(n => n % 2 === 0)
    .map(n => n ** 2)
    .take(5)
    .toArray();

console.log(result1);  // [4, 16, 36, 64, 100]

// Complex pipeline
const result2 = pipeline(range(1, 100))
    .filter(n => n % 3 === 0)
    .drop(5)
    .map(n => n * 2)
    .take(10)
    .reduce((sum, n) => sum + n, 0);

console.log(result2);  // Sum of processed values

// Find first match
const found = pipeline(naturals())
    .filter(n => n % 7 === 0)
    .find(n => n > 100);

console.log(found);  // 105

// Test condition
const hasLarge = pipeline(range(1, 10))
    .map(n => n ** 2)
    .some(n => n > 50);

console.log(hasLarge);  // true
Key Points: Iterator helpers enable functional programming patterns with lazy evaluation. Lazy methods (take, drop, filter, map) return iterators. Eager methods (reduce, forEach, some) consume iterator immediately. Chain helpers for complex transformations. Perfect for large datasets - only processes needed elements. ES2024+ adds native support.

Section 18 Summary: Iterators, Generators, and Iteration Protocols

  • Iterator Protocol: Object with next() returning {value, done}
  • Iterable Protocol: Object with [Symbol.iterator] method
  • Built-in Iterables: Array, String, Map, Set, TypedArray, arguments
  • Generators: function* syntax, pausable execution with yield
  • yield Expressions: yield value, yield* delegation, two-way communication
  • Generator Methods: next(), return(), throw()
  • Async Generators: async function*, for await...of loops
  • Custom Iterators: Implement [Symbol.iterator], track state, optional cleanup
  • Iterator Helpers: take, drop, filter, map, reduce (lazy evaluation)
  • Lazy Evaluation: Process data on demand, efficient for infinite sequences
  • Use Cases: Streaming data, pagination, infinite sequences, resource management