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 enumerationWell-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]"| Symbol | Purpose |
|---|---|
Symbol.iterator | Defines default iteration behavior |
Symbol.asyncIterator | Defines async iteration (for await...of) |
Symbol.toPrimitive | Controls type coercion |
Symbol.toStringTag | Customizes Object.prototype.toString |
Symbol.hasInstance | Controls instanceof behavior |
Symbol.species | Defines 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 foreverCancellable 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:
- Protocol understanding â Iterable vs iterator protocol, how
for...ofconsumes them - Lazy evaluation â Building pipelines that process one item at a time without intermediate arrays
- Async generators â Streaming paginated data, processing large files
- Symbol depth â Well-known symbols for metaprogramming,
Symbol.forregistry, why symbols exist beyond "unique keys" - Real-world usage â Redux-Saga, RxJS, custom collections, state machines