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 ProtocolsAn 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 iterable — for...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.