PureDevTools

JavaScript Iterator & Generator Reference

Iterator protocol, generator functions, and async iterators — with syntax, parameters, edge cases, and a live code runner

All processing happens in your browser. No data is sent to any server.
APIPurposeES
Symbol.iteratorDefines the default iterator for an object.ES2015
for...ofLoops over any iterable object, calling [Symbol.ES2015
next()Advances the iterator by one step and returns { value, done }.ES2015
return()Terminates the iterator early and returns { value, done: true }.ES2015
throw()Injects an exception into the generator at the paused yield point.ES2015
Array.from()Creates a new array from any iterable or array-like object, exhausting the iterator completely.ES2015
spread (...)Expands an iterable's values into an array literal or function arguments.ES2015
Symbol.iteratorIterator ProtocolES2015
obj[Symbol.iterator]() { return iteratorObject; }

Defines the default iterator for an object. Any object that implements a [Symbol.iterator]() method returning an iterator is called an *iterable* — it can be used in for...of loops, spread expressions, destructuring assignments, and Array.from().

for...ofIterator ProtocolES2015
for (const item of iterable) { ... }

Loops over any iterable object, calling [Symbol.iterator]() once and then next() on each iteration until done is true. Works with arrays, strings, Maps, Sets, generators, and any object implementing the iterable protocol.

next()Iterator ProtocolES2015
iterator.next(value?)

Advances the iterator by one step and returns { value, done }. The optional value argument becomes the result of the yield expression inside a generator — enabling two-way communication. When done is true, the iteration is complete.

return()Iterator ProtocolES2015
iterator.return(value?)

Terminates the iterator early and returns { value, done: true }. Inside generators it executes any pending finally blocks, allowing resource cleanup. Called automatically by for...of when the loop exits via break or return.

throw()Iterator ProtocolES2015
iterator.throw(error)

Injects an exception into the generator at the paused yield point. If the generator has a try...catch around the paused yield, it can handle the error and continue. If not caught, the exception propagates to the throw() caller and the generator terminates.

Array.from()Iterator ProtocolES2015
Array.from(iterable, mapFn?)

Creates a new array from any iterable or array-like object, exhausting the iterator completely. Accepts an optional mapping function applied to each element during conversion.

spread (...)Iterator ProtocolES2015
[...iterable] / fn(...iterable)

Expands an iterable's values into an array literal or function arguments. Internally calls [Symbol.iterator]() and exhausts the entire iterator. Can be combined with other values in array literals.

You’re paginating API results — each page is a fetch call, and you want to process items one at a time across pages without loading all 10,000 results into memory. You could write a recursive function with callbacks, or you could use an async generator: async function* fetchPages(url) that yields items one by one and for await...of consumes them. But the iterator protocol, Symbol.iterator vs Symbol.asyncIterator, yield vs yield*, and next()/return()/throw() methods are a lot to hold in your head.

Why This Reference (Not MDN)

MDN splits iterators, generators, async iterators, and async generators across 6+ pages. This reference puts the entire protocol on one page — Symbol.iterator, for...of, next()/return()/throw(), function*, yield, yield*, Symbol.asyncIterator, async function*, and for await...of — with interactive examples.

What Are JavaScript Iterators and Generators?

The iterator protocol and generator functions are JavaScript’s built-in mechanism for lazy, on-demand sequences. Instead of computing all values upfront and storing them in an array, iterators produce values one at a time — only when requested.

// The simplest custom iterator
const counter = {
  [Symbol.iterator]() {
    let n = 0;
    return {
      next() {
        return n < 3
          ? { value: n++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

for (const n of counter) console.log(n); // 0  1  2
console.log([...counter]);               // [0, 1, 2]

The Iterator Protocol

Symbol.iterator — Making Objects Iterable

Any object becomes iterable by implementing [Symbol.iterator](). This method is called once when the iterable enters a for...of loop or a spread expression, and must return an iterator.

class Range {
  constructor(from, to) { this.from = from; this.to = to; }
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
}

const r = new Range(1, 5);
console.log([...r]);              // [1, 2, 3, 4, 5]
console.log(Array.from(r));       // [1, 2, 3, 4, 5]
const [first, second] = r;
console.log(first, second);       // 1  2

for…of — Iterating Over Any Iterable

for...of works with all built-in iterables (Array, String, Map, Set, TypedArray, arguments, NodeList, generators) and any custom object implementing [Symbol.iterator]().

// Arrays
for (const n of [10, 20, 30]) console.log(n); // 10  20  30

// Strings (iterates Unicode code points, not bytes)
for (const ch of "hi 👋") console.log(ch);  // h  i  (space)  👋

// Maps (key-value pairs)
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, val] of map) console.log(key, "->", val); // a -> 1  b -> 2

// Sets (unique values)
for (const v of new Set([1, 2, 2, 3])) console.log(v);  // 1  2  3

next(), return(), throw() — The Iterator Interface

Every iterator exposes three methods. next() is required; return() and throw() are optional but standard:

MethodPurpose
next(value?)Advance one step; returns { value, done }
return(value?)Terminate early; triggers finally in generators
throw(error)Inject an exception at the paused yield
function* source() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("iterator closed");
  }
}

const it = source();
console.log(it.next());          // { value: 1, done: false }
console.log(it.return("done"));  // iterator closed  { value: "done", done: true }

Generator Functions

function* — Declaring a Generator

Generator functions use function* syntax. Calling them does NOT execute the body — it returns a Generator object. The body runs only when next() is called:

function* countdown(n) {
  while (n > 0) {
    yield n--;
  }
  return "blastoff!";
}

const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: "blastoff!", done: true }
console.log(gen.next()); // { value: undefined, done: true }

The generator’s return value appears in the last { done: true } result — but for...of and spread skip it. To capture it you must call next() manually.

yield — Two-Way Communication

yield both produces a value outward and receives a value inward. The value passed to next(value) becomes the result of the paused yield expression:

function* logger() {
  let count = 0;
  while (true) {
    const msg = yield count++;
    if (msg === "reset") count = 0;
    console.log("logged:", msg, "count now:", count);
  }
}

const log = logger();
log.next();           // Start the generator
log.next("a");        // logged: a  count now: 1
log.next("b");        // logged: b  count now: 2
log.next("reset");    // logged: reset  count now: 0

yield* — Delegating to Another Iterable

yield* flattens another iterable into the current generator’s output. It also collects the inner generator’s return value:

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

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

Common Generator Patterns

Infinite Sequence

function* naturals(start = 1) {
  while (true) yield start++;
}

function take(n, iter) {
  const result = [];
  for (const v of iter) {
    result.push(v);
    if (result.length === n) break;
  }
  return result;
}

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

Lazy Pipeline

function* map(iter, fn) {
  for (const v of iter) yield fn(v);
}

function* filter(iter, pred) {
  for (const v of iter) if (pred(v)) yield v;
}

function* take(n, iter) {
  for (const v of iter) {
    yield v;
    if (--n === 0) break;
  }
}

function* naturals() { let n = 0; while (true) yield n++; }

// Find the first 3 even squares — all lazily evaluated
const result = take(3, filter(map(naturals(), n => n * n), n => n % 2 === 0));
console.log([...result]); // [0, 4, 16]

State Machine

function* trafficLight() {
  while (true) {
    yield "green";
    yield "yellow";
    yield "red";
  }
}

const light = trafficLight();
console.log(light.next().value); // green
console.log(light.next().value); // yellow
console.log(light.next().value); // red
console.log(light.next().value); // green

Async Iterators and Generators

Symbol.asyncIterator — Async Iterable Protocol

Objects implement [Symbol.asyncIterator]() to become async iterables. The method returns an async iterator whose next() returns a Promise<{ value, done }>:

async function run() {
  const timedSequence = {
    [Symbol.asyncIterator]() {
      const items = [1, 2, 3];
      let i = 0;
      return {
        async next() {
          await new Promise(r => setTimeout(r, 50));
          return i < items.length
            ? { value: items[i++], done: false }
            : { value: undefined, done: true };
        }
      };
    }
  };

  for await (const n of timedSequence) {
    console.log(n); // 1  2  3  (each with 50ms delay)
  }
}
run();

async function* — Async Generator Functions

Async generators combine await and yield. They are ideal for paginated APIs, streaming responses, and any data that arrives asynchronously:

async function* fetchPages(url, maxPages) {
  let page = 1;
  while (page <= maxPages) {
    // In real code: const data = await fetch(`${url}?page=${page}`).then(r => r.json());
    await new Promise(r => setTimeout(r, 0)); // simulate network
    yield { page, data: ["item" + (page * 2 - 1), "item" + (page * 2)] };
    page++;
  }
}

async function run() {
  for await (const { page, data } of fetchPages("/api/items", 3)) {
    console.log("page", page, "->", data.join(", "));
  }
}

run();

for await…of — Async Iteration Loop

for await...of iterates async iterables, awaiting each item. It also works on sync iterables (wrapping each value with Promise.resolve):

async function* countdown(n) {
  while (n > 0) {
    await new Promise(r => setTimeout(r, 0));
    yield n--;
  }
}

async function run() {
  for await (const n of countdown(3)) {
    console.log(n); // 3  2  1
  }
}
run();

Built-In Iterables Cheat Sheet

ValueSymbol.iterator?Yields
ArrayYesElements
StringYesUnicode code points (not UTF-16 code units)
MapYes[key, value] pairs
SetYesValues (in insertion order)
TypedArrayYesElements
argumentsYesArgument values
NodeListYesDOM nodes
GeneratorYesYielded values (the generator IS its own iterator)
Plain {}No— (use Object.entries / Object.keys)

Frequently Asked Questions

What is the difference between an iterator and an iterable?

An iterator is an object with a next() method returning { value, done }. An iterable is an object with a [Symbol.iterator]() method that returns an iterator. These are separate protocols. Generators satisfy both: the generator object IS its own iterator (gen[Symbol.iterator]() === gen), so it is both an iterable and an iterator.

Can a generator be paused from outside?

No. Generators are cooperative — they pause themselves with yield. The caller cannot forcibly suspend a generator mid-execution. The only way to advance or interact with a generator is through its next(), return(), and throw() methods.

What happens to a generator after it returns?

After a generator’s function body completes (via return or falling off the end), the generator is exhausted. All subsequent next() calls return { value: undefined, done: true }. Calling return() on an exhausted generator is a no-op.

Why does for…of skip the generator’s return value?

The for...of loop stops when it sees { done: true } — and the value property of that last result is ignored. This is by design: the convention is that return values from generators are metadata, not part of the produced sequence. To capture a return value, call next() manually until done is true and read result.value.

How do I convert an async iterable to an array?

Array.from() and spread ([...]) do NOT work with async iterables — they only support sync iterables. To collect all values from an async iterable, use for await...of:

async function toArray(asyncIterable) {
  const result = [];
  for await (const item of asyncIterable) result.push(item);
  return result;
}

When should I use a generator vs a regular function returning an array?

Use a generator when: (1) the sequence is lazy — you want to avoid computing all values upfront; (2) the sequence is infinite; (3) you need early termination and want cleanup via finally; (4) you are building a pipeline of transformations. Use an array when the full result is needed immediately, the sequence is small and finite, or you need random access.

Related Tools

More JavaScript Tools