Iterators & Generators

10 questions found

What makes an object 'iterable' in JavaScript?

Beginner
An object is iterable if it implements the Symbol.iterator method, which returns an iterator object with a next() method. This is what powers for...of loops, spread syntax, and destructuring on that object.
const range = {
  [Symbol.iterator]() {
    let i = 1;
    return { next: () => i <= 3 ? { value: i++, done: false } : { value: undefined, done: true } };
  }
};
console.log([...range]); // [1, 2, 3]
Real-world example Making a custom collection class work seamlessly with for...of and spread, just like a built-in Array.

Common follow-ups: Which built-in types are iterable by default in JavaScript?

Types & Coercion

How do you write a basic generator function?

Beginner
Add an asterisk after 'function' to make it a generator; calling it doesn't run the body immediately but returns an iterator, and each yield pauses execution, resuming from that point on the next next() call.
function* countTo3() {
  yield 1;
  yield 2;
  yield 3;
}
const gen = countTo3();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
Real-world example Lazily generating a sequence of values one at a time instead of computing and storing them all upfront.

Common follow-ups: What does gen.next() return once the generator is finished?

Types & Coercion

Can you use a for...of loop directly on a generator's return value?

Beginner
Yes — a generator function's return value is itself an iterable (it implements Symbol.iterator returning itself), so for...of can consume it directly, automatically calling next() and stopping when done is true.
function* countTo3() { yield 1; yield 2; yield 3; }
for (const n of countTo3()) {
  console.log(n); // 1, 2, 3
}
Real-world example Iterating over a generator-produced sequence with the same clean syntax used for arrays.

Common follow-ups: Does for...of capture the final 'return value' of a generator (the value alongside done: true)?

Async Iterators & Streams

How does yield pause and resume execution, and what does it return to the caller?

Intermediate
When a generator hits yield, execution pauses immediately and control returns to the caller with an object { value, done: false } — the generator's internal state (including local variables) is preserved until next() is called again, which resumes execution right after that yield.
function* logger() {
  console.log('before');
  const received = yield 'paused-value';
  console.log('resumed with:', received);
}
const it = logger();
it.next();           // logs 'before', returns { value: 'paused-value', done: false }
it.next('hello');    // logs 'resumed with: hello'
Real-world example Building a step-by-step wizard flow where each step waits for external input before continuing.

Common follow-ups: How do you pass a value INTO a generator via next(), as opposed to getting one out via yield?

Closures

How do you delegate from one generator to another using yield*?

Intermediate
yield* fully delegates iteration to another iterable (including another generator), yielding each of its values in turn as if they were yielded directly by the outer generator — useful for composing generators without manually re-yielding each value.
function* inner() { yield 'a'; yield 'b'; }
function* outer() {
  yield 1;
  yield* inner(); // delegates to inner()
  yield 2;
}
[...outer()]; // [1, 'a', 'b', 2]
Real-world example Composing a large generator out of smaller, reusable generator building blocks.

Common follow-ups: Can yield* delegate to any iterable, or only to other generators?

Design Patterns in JavaScript

How do you implement an infinite sequence using a generator, and why is this safe?

Intermediate
A generator can loop forever internally (e.g. while (true)) because it only computes and yields one value at a time when next() is called — nothing is precomputed, so it never actually tries to build an infinite array in memory.
function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}
const gen = naturalNumbers();
gen.next().value; // 1
gen.next().value; // 2
Real-world example Generating an ID sequence or lazily producing values for an infinite scroll feed, only as needed.

Common follow-ups: How would you take just the first N values from an infinite generator?

Async Iterators & Streams

How do you use generator.throw() and generator.return() to control a running generator externally?

Advanced
generator.throw(err) resumes the generator by throwing err at the current yield point, letting the generator's own try/catch handle it. generator.return(value) forces the generator to terminate immediately as if a return statement executed there, running any finally blocks first.
function* gen() {
  try {
    yield 1;
  } catch (err) {
    console.log('caught:', err.message);
    yield 2;
  }
}
const it = gen();
it.next();          // { value: 1, done: false }
it.throw(new Error('oops')); // logs 'caught: oops', returns { value: 2, done: false }
Real-world example Cancelling an in-progress generator-based task pipeline cleanly from outside code.

Common follow-ups: What's the difference between calling return() and simply breaking out of a for...of loop over the generator?

Error Handling

How were generators historically used to write asynchronous code before async/await existed?

Advanced
Generators combined with a driver function (like early versions of co or redux-saga) let you yield Promises and have the driver automatically call .next() again once each Promise resolved, producing async-looking code with synchronous-looking syntax — the direct conceptual predecessor to async/await.
function run(genFn) {
  const gen = genFn();
  function step(input) {
    const { value, done } = gen.next(input);
    if (!done) value.then(step);
  }
  step();
}
run(function* () {
  const data = yield fetch('/api').then(r => r.json());
  console.log(data);
});
Real-world example Understanding why redux-saga still uses generators today, and how async/await effectively standardized this pattern.

Common follow-ups: Why did the language eventually add async/await instead of relying purely on generator-driver libraries?

Promises & async/await

How would you implement a custom Symbol.iterator for a tree data structure to enable depth-first traversal with for...of?

Advanced
Implement [Symbol.iterator]() as a generator method (using function*), recursively yielding each node's value and delegating to child nodes with yield* — this lets a complex, non-linear structure be consumed with simple, idiomatic for...of syntax.
class TreeNode {
  constructor(value, children = []) {
    this.value = value;
    this.children = children;
  }
  *[Symbol.iterator]() {
    yield this.value;
    for (const child of this.children) {
      yield* child; // delegates to each child's iterator
    }
  }
}
Real-world example Traversing a nested comment thread or file-system-like tree structure with a plain for...of loop.

Common follow-ups: How would you modify this for breadth-first instead of depth-first traversal?

Design Patterns in JavaScript

What's the performance and memory advantage of a generator-based lazy pipeline over building intermediate arrays with map/filter?

Advanced
Chaining array methods like map().filter() creates a full new intermediate array at each step, even if you only need a few results. A generator-based pipeline processes and yields one item at a time end-to-end, avoiding intermediate arrays entirely — especially valuable for large or infinite sequences where you only consume a subset.
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 result = [...take(filter(map(hugeSource, x => x * 2), x => x > 100), 5)];
Real-world example Processing the first 5 matching results from a huge or infinite dataset without transforming the entire dataset first.

Common follow-ups: At what dataset size does this lazy approach actually start to matter for performance?

Functional Programming