Iterators, generators, Map and Set

Symbol.iterator, generator functions and yield, lazy sequences, for...of, when a Map beats an object, and the Set operations you actually need.

The iterator protocol

An iterable has a method at Symbol.iterator returning an object with a next() method. for...of, spread, destructuring and Array.from all consume that interface, so implementing it is what makes your own object work with them.

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      },
      [Symbol.iterator]() { return this; },   // an iterator is itself iterable
    };
  },
};

[...range];                 // [1, 2, 3]
Array.from(range);          // [1, 2, 3]
const [a, b] = range;       // 1, 2
for (const n of range) console.log(n);

// partially consuming an iterator, then handing it on
const it = range[Symbol.iterator]();
it.next();                  // { value: 1, done: false }
for (const n of it) console.log(n);   // 2, 3 - continues from where it stopped
  • A for...of loop calls return() on the iterator when it exits early — that is how cleanup runs in a generator.
  • Strings, arrays, Map, Set, NodeList and generators are all iterable; plain objects are not.
  • Iterators are single-use. Spread consumes them completely, so a second spread finds nothing.

Generator functions

A function* returns a generator that pauses at each yield and resumes with local state intact. The body does not run at all until you ask for the first value, so a generator can express an unbounded or very large sequence without allocating it.

function* takeWhile(iterable, predicate) {
  for (const item of iterable) {
    if (!predicate(item)) return;      // return() is also triggered here
    yield item;
  }
}

function* naturals() {
  let n = 1;
  while (true) yield n++;              // infinite, but only computed on demand
}

const small = [...takeWhile(naturals(), n => n < 5)];   // [1, 2, 3, 4]

// a generator can receive values back in
function* running() {
  let total = 0;
  for (;;) {
    const value = yield total;         // sent value arrives here
    total += value ?? 0;
  }
}
const g = running();
g.next();          // { value: 0, done: false } - primes it
g.next(10);        // { value: 10, done: false }
g.next(5);         // { value: 15, done: false }

// async generators: streams of awaited values
async function* pages(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    const body = await res.json();
    yield body.items;
    next = body.nextUrl;
  }
}

for await (const batch of pages('/api/items')) {
  process(batch);                      // one page in memory at a time
}
⚠️
An error thrown inside a generator propagates to the consumer at the next() call, not where the generator was created — so wrap the loop, not the call. Also remember that a generator you never start never runs: a for await over an unused async generator performs no work at all.

Map, Set and WeakMap

const m = new Map([['a', 1]]);
m.set({ id: 7 }, 'object key');        // any key type, including objects
m.get('a');                            // 1
m.has('a');                            // true
m.size;                                // 2
for (const [key, value] of m) console.log(key, value);
const obj = Object.fromEntries(m);     // only when keys are strings

const s = new Set([1, 2, 2, 3]);       // deduplicated on construction
s.add(4); s.delete(1); s.size;         // 3
[...s];                                // [2, 3, 4] - insertion order

// set algebra
const a = new Set([1, 2, 3]), b = new Set([2, 3, 4]);
new Set([...a].filter(x => b.has(x)));              // intersection
new Set([...a, ...b]);                              // union
new Set([...a].filter(x => !b.has(x)));             // difference
a.isSubsetOf(b);                                    // modern runtimes
a.intersection(b);                                  // newer still - check support

// dedupe objects by one property
const byId = new Map(users.map(u => [u.id, u]));
const unique = [...byId.values()];

// non-iterable side table keyed by object, garbage-collected with it
const cache = new WeakMap();
function memo(obj, compute) {
  if (!cache.has(obj)) cache.set(obj, compute(obj));
  return cache.get(obj);
}
NeedUseReason
Fixed record shape, JSON outputObjectSerialises directly
Frequent add and deleteMapTuned for mutation and gives size
Keys that are objectsMap or WeakMapObject keys are coerced to strings
Membership tests on many valuesSetAverage O(1) lookup instead of O(n) on an array
Attach data to a DOM node or objectWeakMapNo memory leak when the key is collected
Unique values from an arraynew Set(arr)Idiomatic one-liner; order is preserved

A Map is not JSON-serialisable, so convert with Object.fromEntries at the boundary and keep the Map inside your own code. A WeakMap cannot be iterated by design — if you need to list the entries, you need a real Map plus your own cleanup.

FAQ

How do I pick between a generator and an array method?
Generators win when the sequence is large, infinite, or produced by an IO step, because nothing is computed until it is needed and the chain stays memory-flat. Arrays are simpler when you need length, repeated passes or random access.
Why is my generator returning an empty result?
It was already consumed earlier — spread, Array.from or a previous loop exhausted it. Recreate it by calling the generator function again rather than reusing the object.

Classes, prototypes and object-oriented patterns The event loop, timers and concurrency

Last refreshed 2026-09-18.