Generators & Iterators Interview Questions & Answers

30 questions Updated 2026-06-18

JavaScript generators and iterators interview questions — the iterator and iterable protocols, function* and yield, yield* delegation, lazy and infinite sequences, two-way communication and async generators.

Read the in-depth guideJavaScript Generators & Iterators Explained — Lazy Sequences and the Iteration Protocols

An iterator is any object with a next() method that returns { value, done } on each call — done: true signals the end.

const it = {
  i: 0,
  next() {
    return this.i < 3
      ? { value: this.i++, done: false }
      : { value: undefined, done: true }   // terminator
  }
}
it.next()  // { value: 0, done: false }

Anything implementing this shape can be driven manually, but usually you consume iterators through for...of, spread, or destructuring.

An iterable is an object with a [Symbol.iterator]() method that returns an iterator. The iterable is the collection; the iterator is the cursor over it.

const range = {
  [Symbol.iterator]() {
    let n = 0
    return { next: () => n < 3
      ? { value: n++, done: false }
      : { value: undefined, done: true } }
  }
}
[...range]   // [0, 1, 2]  now spreadable / for-of-able

for...of, spread, Array.from, and destructuring all call Symbol.iterator under the hood.

Arrays, strings, Map, Set, arguments, TypedArray, and DOM collections like NodeList — all ship a Symbol.iterator.

for (const ch of 'hi') {}        // strings
for (const [k, v] of map) {}     // Map yields [key, value]

Notably plain objects are NOT iterablefor...of {} throws. Use Object.keys/values/entries, which return iterable arrays.

A generator (function*) is a function that can pause and resume. Calling it doesn't run the body — it returns a generator object (which is both an iterator and iterable). Each next() runs to the next yield.

function* gen() {
  yield 1
  yield 2
}
const g = gen()
g.next()  // { value: 1, done: false }
g.next()  // { value: 2, done: false }
g.next()  // { value: undefined, done: true }

Generators are the easiest way to build custom iterators without writing next() by hand.

yield pauses the generator and emits a value to the caller. Execution freezes — local variables and position are preserved — until the next next() resumes it right after that yield.

function* counter() {
  let n = 0
  while (true) yield n++   // resumes here each time
}

This pause/resume is what makes lazy and infinite sequences possible.

A generator object implements both protocols: it has next() (it's an iterator) and a Symbol.iterator that returns itself (it's iterable).

function* g() { yield 'a'; yield 'b' }
[...g()]                 // ['a', 'b']
for (const x of g()) {}  // works directly

Because it returns itself from Symbol.iterator, a generator is a one-shot iterable — once consumed it's exhausted.

Define [Symbol.iterator] as a generator method; yield the elements and the protocol bookkeeping is handled for you.

class Range {
  constructor(s, e) { this.s = s; this.e = e }
  *[Symbol.iterator]() {
    for (let i = this.s; i < this.e; i++) yield i  //
  }
}
[...new Range(1, 4)]   // [1, 2, 3]

Compare to the manual next()/done version — the generator removes all the state plumbing.

Values are produced on demand, only when next() asks. Nothing is computed until consumed, and computation stops as soon as the consumer stops.

function* squares() { let n = 1; while (true) yield n * n++ }
const it = squares()
it.next().value  // 1   — only this much computed
it.next().value  // 4

This lets you model infinite or expensive sequences and pull just the first few elements without computing the rest.

A generator with an unbounded loop is fine as long as the consumer bounds it.

function* naturals() { let n = 1; while (true) yield n++ }
function take(it, k) {
  const out = []
  for (const x of it) { out.push(x); if (out.length === k) break }
  return out
}
take(naturals(), 3)   // [1, 2, 3]

The pitfall: [...naturals()] or for...of without a break will loop forever. Always cap consumption.

yield* delegates to another iterable/generator — it yields all of its values in place, as if inlined.

function* inner() { yield 1; yield 2 }
function* outer() {
  yield 0
  yield* inner()   // yields 1 then 2
  yield 3
}
[...outer()]   // [0, 1, 2, 3]

It also forwards the delegated generator's return value as the result of the yield* expression, useful for composing generators.

Yes — yield* works on any iterable, so you can flatten arrays, strings, Sets, etc.

function* flatten(arrs) {
  for (const a of arrs) yield* a   // spread each array's items
}
[...flatten([[1, 2], [3], [4, 5]])]  // [1, 2, 3, 4, 5]

This makes recursive flattening of nested structures elegant — just yield* flatten(child) for sub-arrays.

The argument to next(value) becomes the result of the yield expression that the generator is paused on — two-way communication.

function* convo() {
  const name = yield 'What is your name?'
  yield `Hello, ${name}!`
}
const c = convo()
c.next().value        // 'What is your name?'
c.next('Ada').value   // 'Hello, Ada!'  'Ada' became `name`

Gotcha: the first next() argument is ignored — there's no paused yield yet to receive it.

gen.return(v) forces the generator to finish early, yielding { value: v, done: true } and running any finally blocks for cleanup.

function* g() {
  try { yield 1; yield 2 }
  finally { console.log('cleanup') }  // runs on early return
}
const it = g()
it.next()        // { value: 1, done: false }
it.return(99)    // logs 'cleanup'; { value: 99, done: true }

for...of calls return() automatically when you break, so resources get released.

gen.throw(err) injects an exception at the paused yield, as if the yield itself threw — the generator can try/catch it.

function* g() {
  try { yield 1 }
  catch (e) { yield `caught ${e}` }  //
}
const it = g()
it.next()          // { value: 1 }
it.throw('boom')   // { value: 'caught boom', done: false }

If the generator doesn't catch it, the error propagates to the caller of throw().

A return x sets { value: x, done: true } on the final next() — but for...of and spread ignore that value; they only collect yielded values.

function* g() { yield 1; return 99 }
[...g()]                  // [1]  — 99 dropped
const it = g()
it.next()                 // { value: 1, done: false }
it.next()                 // { value: 99, done: true }  visible here

To observe the return value you must drive next() manually or capture it via yield*.

Sequential yields with a surrounding loop naturally encode states — execution position is the current state, so you avoid explicit state variables.

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

The generator remembers where it paused, so each next() advances the machine one transition with no bookkeeping.

When the sequence is large, infinite, or expensive, and the consumer may not need all of it. A generator streams values lazily; an array materializes everything up front.

// reads a huge file line by line without loading it all
function* lines(text) {
  for (const line of text.split('\n')) yield line.trim()
}

For small, fully-consumed collections an array is simpler and faster to index — generators shine on streaming/pipeline workloads.

for...of calls the iterator's return() on break, throw, or an exception — so a generator's finally runs and resources are released.

function* withResource() {
  try { while (true) yield acquire() }
  finally { release() }   // runs even on early break
}
for (const x of withResource()) { if (done) break }  // release() called

This automatic cleanup is a key reason to wrap resource handling in try/finally inside generators.

A generator object is a single-use iterator — once done, it stays done. It doesn't restart.

const g = (function* () { yield 1; yield 2 })()
[...g]   // [1, 2]
[...g]   // []  already exhausted

To re-iterate, expose a factory (a function returning a fresh generator) or a class whose [Symbol.iterator] creates a new one each call.

An async function* can await inside and yield values asynchronously. Its next() returns a Promise of { value, done }, and you consume it with for await...of.

async function* pages(url) {
  let next = url
  while (next) {
    const res = await fetch(next)   // await inside
    const data = await res.json()
    yield data.items
    next = data.nextPage
  }
}
for await (const items of pages('/api')) { /* ... */ }

Perfect for paginated APIs and streams where each chunk arrives over time.

It iterates an async iterable (one with Symbol.asyncIterator), awaiting each next() Promise before the loop body runs. It must be inside an async function.

async function read(stream) {
  for await (const chunk of stream) {   // awaits each chunk
    process(chunk)
  }
}

It also works on a plain array of Promises, awaiting each in turn — handy for sequential async processing.

Symbol.iterator returns an iterator whose next() yields plain { value, done }; Symbol.asyncIterator returns one whose next() yields a Promise of that. for await...of looks for asyncIterator first, falling back to the sync one.

const stream = {
  async *[Symbol.asyncIterator]() {
    yield await getChunk()   // async iterable
  }
}

Use the async variant when each value depends on I/O or timing.

They hold only the current value and the suspended call frame, not the whole sequence — so you can process gigabytes by streaming a constant amount of memory.

function* range(n) { for (let i = 0; i < n; i++) yield i }
let sum = 0
for (const i of range(1e9)) sum += i   // no billion-element array

Building Array.from({length: 1e9}) first would blow the heap; the generator never allocates it.

Chain generators that each take an iterable and yield a transformed one — lazy map/filter that never build intermediate arrays.

function* map(it, f) { for (const x of it) yield f(x) }
function* filter(it, p) { for (const x of it) if (p(x)) yield x }

const result = map(
  filter(range(100), n => n % 2 === 0),
  n => n * n
)   // nothing computed until consumed

Each stage pulls one value at a time, so a pipeline over an infinite source still works.

No — there is no arrow generator syntax. Generators must be function* declarations/expressions or *method() shorthand in objects/classes.

const g = function* () { yield 1 }      // expression
const obj = { *gen() { yield 1 } }      // method shorthand
// const bad = *() => {}                // SyntaxError

As object/class methods, generators get this from the call site like any method, which is exactly why arrow generators aren't needed.

Modern engines add lazy helper methods directly on iterators — map, filter, take, drop, flatMap, reduce, toArray — so you get array-like chaining without materializing arrays.

function* naturals() { let n = 1; while (true) yield n++ }
naturals()
  .filter(n => n % 2)
  .map(n => n * n)
  .take(3)
  .toArray()           // [1, 9, 25]  lazy, bounded by take

They make working with infinite generators ergonomic; check target support as adoption is recent.

A generator manages all the protocol state for you — the { value, done } shape, the done terminator, returning itself from Symbol.iterator, and return()/throw() semantics. Hand-rolled iterators are verbose and error-prone.

// generator: 3 lines vs a dozen for the manual next()/done version
function* range(s, e) { for (let i = s; i < e; i++) yield i }

Reserve manual iterators for cases needing unusual control over the protocol; otherwise use a generator.

Both consume an iterable via its iterator. Destructuring pulls only as many values as the pattern needs; spread drains it fully.

function* g() { yield 1; yield 2; yield 3 }
const [a, b] = g()     // pulls 2, iterator left at 3
const all = [...g()]   // [1, 2, 3] — fully drained

Because destructuring stops early, const [first] = infinite() is safe, but const [...rest] = infinite() hangs.

yield* evaluates to the return value of the inner generator, letting you build sub-results.

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

This is how generators compose computations: the yielded stream flows through while the return value bubbles back to the delegator.

For small, fully-consumed collections, plain arrays/array methods are simpler, faster to index, and easier to debug. Generators add overhead per next() call and can't be randomly accessed or reused.

// overkill — just use an array
function* three() { yield 1; yield 2; yield 3 }
const arr = [1, 2, 3]   // clearer here

Reach for generators when you genuinely need laziness, infinite/streaming data, two-way communication, or memory-bounded processing.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.