DNA⚡ JavaScriptGenerators, Iterators & Symbols
ðŸĶ–DinosaurJavaScriptGeneratorsIteratorsSymbols

Generators, Iterators & Symbols

Generators produce values on demand. Iterators define traversal protocols. Symbols create invisible, collision-free property keys. Together they power async flows, custom collections, and metaprogramming.

Generators, Iterators & Symbols

These three features sit at the intersection of JavaScript's protocol system. Understanding them unlocks lazy evaluation, custom iteration, async generators, and well-known symbol hooks that libraries like Redux-Saga and RxJS depend on.

The Iteration Protocol

JavaScript defines two protocols that any object can implement:

Iterable Protocol:  object has [Symbol.iterator]() → returns an Iterator
Iterator Protocol:  object has next() → returns { value, done }

Arrays, Maps, Sets, and Strings are all iterable. for...of, spread, and destructuring all consume iterables.

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

Custom Iterable

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { done: true };
      },
    };
  },
};
 
for (const n of range) console.log(n); // 1, 2, 3, 4, 5
const nums = [...range]; // [1, 2, 3, 4, 5]

Generator Functions

Generators are functions that can pause and resume. They produce iterators automatically:

function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
 
const fib = fibonacci();
fib.next(); // { value: 0, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 1, done: false }
fib.next(); // { value: 2, done: false }
fib.next(); // { value: 3, done: false }

Lazy Evaluation

Generators produce values on demand — they don't compute the entire sequence upfront:

function* naturals() {
  let n = 1;
  while (true) yield n++;
}
 
function* take(iterable, count) {
  let i = 0;
  for (const item of iterable) {
    if (i++ >= count) return;
    yield item;
  }
}
 
function* map(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}
 
function* filter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}
 
const firstTenSquaresOfEvens = take(
  map(
    filter(naturals(), n => n % 2 === 0),
    n => n * n,
  ),
  10,
);
 
[...firstTenSquaresOfEvens]; // [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

No intermediate arrays are created — each value flows through the pipeline one at a time.

Two-Way Communication with yield

yield can both produce and receive values:

function* accumulator() {
  let total = 0;
  while (true) {
    const value = yield total;
    total += value ?? 0;
  }
}
 
const acc = accumulator();
acc.next();     // { value: 0, done: false } — primes the generator
acc.next(10);   // { value: 10, done: false }
acc.next(20);   // { value: 30, done: false }
acc.next(5);    // { value: 35, done: false }

Generator Delegation with yield*

function* inner() {
  yield 'a';
  yield 'b';
  return 'inner-done';
}
 
function* outer() {
  yield 1;
  const result = yield* inner();
  yield 2;
}
 
[...outer()]; // [1, 'a', 'b', 2]

yield* delegates to another iterable, forwarding next() calls through.

Async Generators

Combine generators with async/await for streaming data:

async function* fetchPages(url) {
  let page = 1;
  while (true) {
    const response = await fetch(`${url}?page=${page}`);
    const data = await response.json();
    if (data.length === 0) return;
    yield data;
    page++;
  }
}
 
for await (const page of fetchPages('/api/users')) {
  renderUsers(page);
}

This pattern is ideal for paginated APIs, WebSocket message streams, and file processing:

async function* readLines(filePath) {
  const reader = fs.createReadStream(filePath);
  let buffer = '';
  for await (const chunk of reader) {
    buffer += chunk;
    const lines = buffer.split('\n');
    buffer = lines.pop();
    yield* lines;
  }
  if (buffer) yield buffer;
}

Symbols

Symbols are unique, immutable primitive values used as property keys:

const id = Symbol('id');
const obj = { [id]: 42, name: 'test' };
 
obj[id];         // 42
Object.keys(obj); // ['name'] — symbols are hidden from enumeration

Well-Known Symbols

JavaScript defines symbols that hook into language mechanics:

class MyCollection {
  #items;
  constructor(...items) { this.#items = items; }
 
  [Symbol.iterator]() { return this.#items[Symbol.iterator](); }
 
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.#items.length;
    if (hint === 'string') return `Collection(${this.#items.join(', ')})`;
    return true;
  }
 
  get [Symbol.toStringTag]() { return 'MyCollection'; }
}
 
const c = new MyCollection(1, 2, 3);
[...c];                  // [1, 2, 3]
+c;                      // 3
`${c}`;                  // "Collection(1, 2, 3)"
Object.prototype.toString.call(c); // "[object MyCollection]"
SymbolPurpose
Symbol.iteratorDefines default iteration behavior
Symbol.asyncIteratorDefines async iteration (for await...of)
Symbol.toPrimitiveControls type coercion
Symbol.toStringTagCustomizes Object.prototype.toString
Symbol.hasInstanceControls instanceof behavior
Symbol.speciesDefines constructor for derived objects

Symbol Registry

Symbol.for() creates globally shared symbols:

const s1 = Symbol.for('app.userId');
const s2 = Symbol.for('app.userId');
s1 === s2; // true — same registry key
 
Symbol.keyFor(s1); // 'app.userId'

Practical Applications

State Machines with Generators

function* trafficLight() {
  while (true) {
    yield 'green';
    yield 'yellow';
    yield 'red';
  }
}
 
const light = trafficLight();
light.next().value; // 'green'
light.next().value; // 'yellow'
light.next().value; // 'red'
light.next().value; // 'green' — cycles forever

Cancellable Async Operations

function* saga() {
  try {
    const user = yield call(fetchUser, userId);
    const posts = yield call(fetchPosts, user.id);
    yield put({ type: 'LOAD_SUCCESS', payload: posts });
  } catch (error) {
    yield put({ type: 'LOAD_FAILURE', error });
  }
}

This is the Redux-Saga pattern — generators make async flows testable and cancellable because the saga runner controls when next() is called.

Interview Signal

Senior candidates demonstrate:

  1. Protocol understanding — Iterable vs iterator protocol, how for...of consumes them
  2. Lazy evaluation — Building pipelines that process one item at a time without intermediate arrays
  3. Async generators — Streaming paginated data, processing large files
  4. Symbol depth — Well-known symbols for metaprogramming, Symbol.for registry, why symbols exist beyond "unique keys"
  5. Real-world usage — Redux-Saga, RxJS, custom collections, state machines