JavaScript - Iterables & Generators

Overview

Estimated time: 35–45 minutes

Iterables power for...of, spread [...x], and many APIs. Learn the iterable/iterator protocols, write custom iterables, and use generators (including async generators) to build lazy, composable data pipelines.

Learning Objectives

  • Explain the iterable and iterator protocols (Symbol.iterator, next()).
  • Use for...of, spread, and destructuring with iterables correctly.
  • Implement custom iterables and generators; delegate with yield*.
  • Build lazy pipelines and understand when to prefer iterables over arrays.
  • Use async iterables and for await...of for streaming and asynchronous sequences.

Prerequisites

Iterable and iterator protocols

An iterable has a [Symbol.iterator]() method that returns an iterator. An iterator has next(), returning { value, done }.

const arr = [1,2,3];          // arrays are iterable
const it = arr[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }

// for...of calls [Symbol.iterator]() to get a fresh iterator
for (const x of arr) { /* ... */ }

// Strings are iterable by code points (with 'u' awareness in some APIs)
for (const ch of 'hi') { /* 'h', 'i' */ }

Using iterables

// Spread and destructuring
const set = new Set([3,1,2]);
const a = [...set];          // [3,1,2]
const [first, ...rest] = set; // first=3, rest=[1,2]

// Map/Set iteration yields [key, value] and values respectively
const m = new Map([["x",1],["y",2]]);
for (const [k,v] of m) { /* ... */ }

Custom iterable

// Range iterable as a plain object
const range = (start, end, step = 1) => ({
  [Symbol.iterator]() {
    let cur = start;
    return {
      next() {
        if ((step > 0 && cur > end) || (step < 0 && cur < end)) return { done: true };
        const v = cur; cur += step; return { value: v, done: false };
      }
    };
  }
});

[...range(1,4)]; // [1,2,3,4]

Generators

A generator function (function*) returns an iterator whose next() steps through yielded values. yield* delegates to another iterable.

function* genRange(start, end, step = 1) {
  for (let x = start; step > 0 ? x <= end : x >= end; x += step) {
    yield x;
  }
}

// Delegation with yield*
function* oddsUpTo(n) {
  for (const x of genRange(1, n)) if (x % 2) yield x;
}
[...oddsUpTo(7)]; // [1,3,5,7]

// Controlling a generator
const g = genRange(1, 2);
g.next();         // { value:1, done:false }
g.return('bye');  // { value:'bye', done:true }

Lazy pipelines with generators

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; }
function* take(iter, n) { let i=0; for (const x of iter) { if (i++ >= n) return; yield x; } }

const pipeline = take(filter(map(genRange(1, 1000), x => x * 2), x => x % 3 === 0), 5);
[...pipeline]; // [6,12,18,24,30]

Async iterables

Async iterables use [Symbol.asyncIterator]() and are consumed with for await...of. Async generators are declared with async function*.

async function* countAsync(n, delayMs = 200) {
  for (let i = 1; i <= n; i++) {
    await new Promise(r => setTimeout(r, delayMs));
    yield i;
  }
}

// Usage (inside an async function):
// for await (const x of countAsync(3)) { console.log(x); }

Common Pitfalls

  • One-shot iterators: Some iterables return stateful iterators that cannot be reused once consumed.
  • for...in vs for...of: for...in iterates keys on objects; use for...of for values from iterables.
  • Infinite generators: Ensure termination or limits when spreading or collecting to arrays.
  • Serialization: Iterators/generators are not JSON-serializable; convert to arrays or objects first.

Try it

Run to experiment with custom iterables, generators, and async iteration: