Advanced0 questionsFull Guide

JavaScript Iterator & Iterable Interview Questions

Iterators and iterables are the protocol behind for...of, spread, and destructuring. Learn to implement custom iterables.

The Mental Model

Picture a book with a bookmark. The book is the data. The bookmark knows your current position. Every time you want the next page, you ask the bookmark — it gives you that page and moves forward. You don't know how many pages are left. You don't need to know. You just keep asking until the bookmark tells you there are no more pages. That bookmark is an iterator. The book is an iterable. An iterable is anything that can hand you a fresh bookmark — a new iterator — when asked. Once you have an iterator, you ask it for the next value by calling .next(). Each call returns a small object: { value: ..., done: false } until the sequence is exhausted, at which point it returns { value: undefined, done: true }. The key insight: this is a protocol, not a data structure. Any object that implements the right methods follows the protocol and automatically works with for...of loops, spread syntax, destructuring, Array.from(), Promise.all(), and every other part of the language that consumes sequences. You can make your own objects iterable — a range, a paginated API, an infinite sequence — and they'll plug into all of JavaScript's iteration machinery for free.

The Explanation

The iterator protocol

An iterator is any object with a next() method that returns { value, done }.

// A manual iterator — counting from 1 to 3
const counter = {
  current: 1,
  last: 3,
  next() {
    if (this.current <= this.last) {
      return { value: this.current++, done: false }
    }
    return { value: undefined, done: true }
  }
}

counter.next()  // { value: 1, done: false }
counter.next()  // { value: 2, done: false }
counter.next()  // { value: 3, done: false }
counter.next()  // { value: undefined, done: true }
counter.next()  // { value: undefined, done: true } — stays exhausted

The iterable protocol

An iterable is any object with a [Symbol.iterator]() method that returns a fresh iterator.

// Making an object iterable — the range example
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 }
          : { value: undefined, done: true }
      }
    }
  }
}

// Now range works with EVERYTHING that consumes iterables:
for (const n of range) console.log(n)  // 1 2 3 4 5
const arr = [...range]                 // [1, 2, 3, 4, 5]
const [first, second] = range          // first=1, second=2
Array.from(range)                      // [1, 2, 3, 4, 5]
new Set(range)                         // Set {1, 2, 3, 4, 5}
Math.max(...range)                     // 5

Built-in iterables

// All of these implement [Symbol.iterator]:
// Array, String, Map, Set, TypedArray, NodeList, arguments, generators

// String — iterates by Unicode code point (not UTF-16 code unit!)
for (const char of 'hello') console.log(char)  // h, e, l, l, o
[...'hello']  // ['h', 'e', 'l', 'l', 'o']

// Correctly handles emoji (multi-code-unit characters):
[...'👋🌍']   // ['👋', '🌍'] — correct: 2 items
'👋🌍'.split('')  // ['?', '?', '?', '?'] — wrong: splits UTF-16 code units

// Map — iterates as [key, value] pairs
const map = new Map([['a', 1], ['b', 2]])
for (const [key, val] of map) console.log(key, val)

// Set — iterates values in insertion order
for (const val of new Set([1, 2, 3])) console.log(val)

// arguments object — iterable in modern JS
function sum() {
  return [...arguments].reduce((a, b) => a + b, 0)
}

Generators — iterators made simple

Writing iterators manually requires boilerplate. Generator functions produce iterators automatically, with yield providing each value.

// Generator function — asterisk after function keyword
function* range(from, to) {
  for (let i = from; i <= to; i++) {
    yield i  // pause, return this value, resume from here next time
  }
}

const r = range(1, 5)
r.next()  // { value: 1, done: false }
r.next()  // { value: 2, done: false }
// ...
r.next()  // { value: 5, done: false }
r.next()  // { value: undefined, done: true }

// Generator IS an iterable too (it has [Symbol.iterator] that returns itself)
for (const n of range(1, 5)) console.log(n)  // 1 2 3 4 5
[...range(1, 5)]  // [1, 2, 3, 4, 5]

// Infinite sequence — no problem, we pull values on demand (lazy)
function* naturals() {
  let n = 1
  while (true) yield n++
}

const gen = naturals()
gen.next().value   // 1
gen.next().value   // 2
gen.next().value   // 3
// Never allocates a full array — one value at a time

yield* — delegating to another iterable

function* concat(...iterables) {
  for (const iterable of iterables) {
    yield* iterable  // yield every value from iterable
  }
}

[...concat([1, 2], [3, 4], [5])]  // [1, 2, 3, 4, 5]

// Flatten arbitrary depth:
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) yield* flatten(item)
    else yield item
  }
}

[...flatten([1, [2, [3, [4]]]])]  // [1, 2, 3, 4]

Two-way communication with generators

// yield can also RECEIVE values via next(value)
function* adder() {
  let total = 0
  while (true) {
    const n = yield total  // yield current total, receive next number
    total += n
  }
}

const add = adder()
add.next()     // { value: 0, done: false } — start it (first next() ignores arg)
add.next(10)   // { value: 10, done: false } — added 10
add.next(20)   // { value: 30, done: false } — added 20
add.next(5)    // { value: 35, done: false } — added 5

// This two-way channel is exactly what async generators and redux-saga use
// to coordinate async workflows

Async iteration — iterating Promises

// for await...of — consume async iterables
async function* paginate(url) {
  let page = 1
  while (true) {
    const data = await fetch(`${url}?page=${page}`).then(r => r.json())
    if (data.items.length === 0) return  // done
    yield data.items
    page++
  }
}

// Consume page by page — only fetches next page when ready for it
async function processAll() {
  for await (const items of paginate('/api/products')) {
    items.forEach(processItem)
  }
}

// Node.js Readable streams are async iterables:
const { createReadStream } = require('fs')
async function readFile(path) {
  const lines = []
  for await (const chunk of createReadStream(path, { encoding: 'utf8' })) {
    lines.push(chunk)
  }
  return lines.join('')
}

Custom iterable class — practical example

class LinkedList {
  constructor() { this.head = null }

  push(value) {
    this.head = { value, next: this.head }
  }

  // Make LinkedList iterable — works with for...of, spread, destructuring
  [Symbol.iterator]() {
    let node = this.head
    return {
      next() {
        if (node) {
          const value = node.value
          node = node.next
          return { value, done: false }
        }
        return { value: undefined, done: true }
      }
    }
  }
}

const list = new LinkedList()
list.push(3); list.push(2); list.push(1)

for (const val of list) console.log(val)  // 1 2 3
const arr = [...list]                     // [1, 2, 3]
const [first] = list                      // first = 1

Common Misconceptions

⚠️

Many devs think for...of and for...in are similar loops — but actually they work on completely different protocols. for...of consumes iterables via the [Symbol.iterator] protocol and gives you values. for...in enumerates object property keys (including inherited ones) and gives you strings. They're unrelated. Using for...in on an array gives you the indices as strings ('0', '1', '2'), plus any non-numeric enumerable properties — almost never what you want.

⚠️

Many devs think iterators and iterables are the same thing — but actually they're two separate roles. An iterable is an object that has [Symbol.iterator]() and returns an iterator. An iterator is an object that has next() and returns { value, done }. An object can be both — generators are their own iterator (their [Symbol.iterator]() returns themselves). A plain range object with only [Symbol.iterator]() is iterable but not an iterator.

⚠️

Many devs think spreading an iterator twice gives you the same result both times — but actually iterators are stateful and consumed. Once an iterator reaches done: true, it's exhausted. Spreading it again gives you an empty result. Iterables, however, return a fresh iterator each time [Symbol.iterator]() is called — which is why you can for...of the same array multiple times. If you need to spread a generator twice, call the generator function twice to get two fresh iterators.

⚠️

Many devs think Array.from() only works on array-like objects — but actually Array.from() accepts any iterable (anything with [Symbol.iterator]) as well as array-like objects (anything with .length and numeric indices). This means Array.from() works on strings, Sets, Maps, generators, NodeLists, and any custom iterable. It also accepts an optional mapping function as its second argument: Array.from({length: 5}, (_, i) => i * 2) creates [0, 2, 4, 6, 8].

⚠️

Many devs think you need to fully consume an iterator — but actually you can stop early. for...of with break, return, or throw calls the iterator's optional return() method, which allows the iterator to clean up resources. This is critical for iterators that hold open file handles, database cursors, or network connections. Generators implement return() automatically — breaking out of a for...of over a generator properly terminates the generator.

⚠️

Many devs think generators are just for simple sequences — but actually generators are the foundation of more powerful async coordination patterns. Redux-Saga uses generators for managing complex async flows. The async/await syntax itself was inspired by generators (and originally implemented using them in transpilers like Babel). Generators' ability to suspend and resume execution, combined with two-way value passing via next(value), makes them a general-purpose coroutine mechanism.

Where You'll See This in Real Code

Redux-Saga uses generators as its core abstraction for managing async side effects in Redux applications. A saga is a generator function where each yield is an effect descriptor — yield call(fetchUser, id) or yield put(userLoaded(user)). The saga middleware consumes the iterator, runs each effect, and feeds results back via next(value). This makes async flows look synchronous, be fully testable (test the yielded descriptors, not the actual async calls), and be cancellable by calling the iterator's return() method.

Node.js streams are async iterables — every Readable stream implements [Symbol.asyncIterator]. This means you can process large files line by line with for await...of without loading the entire file into memory: for await (const line of readline.createInterface({ input: fs.createReadStream(file) })). Stream backpressure is handled automatically — the loop pauses awaiting next values when the consumer is slower than the producer.

The DOM's NodeList, HTMLCollection, and many Web API collections are iterables — document.querySelectorAll() returns a NodeList that you can spread, destructure, or iterate with for...of. This wasn't always true (older NodeLists weren't iterable), which is why old code uses Array.from(document.querySelectorAll(...)) or Array.prototype.slice.call(). Modern code can simply write [...document.querySelectorAll('li')].

Infinite scroll pagination with async generators is a clean architecture pattern — define an async generator that fetches pages and yields items, then consume it with for await...of, stopping when you've rendered enough items or the user stops scrolling. The generator holds all pagination state internally (page number, has-more flag) and the consumer doesn't need to manage any of that. This is lazy evaluation applied to API consumption.

TypeScript's IterableIterator<T> type is the return type of all built-in iterators, and properly typing custom iterables requires implementing both [Symbol.iterator](): Iterator<T> and next(): IteratorResult<T>. Understanding the iterator/iterable distinction is necessary to correctly type data structures like linked lists, trees, or graphs that you make iterable — a frequent requirement in TypeScript-heavy codebases.

Babel's original async/await transpilation used generators — before native async/await support, Babel converted async functions to generator functions and used a runtime helper (regeneratorRuntime) to manage the state machine. The resulting code called next() and handled yielded Promises manually. This is why understanding generators gives you a complete mental model of what async/await does at the machine level.

Interview Cheat Sheet

  • Iterator protocol: object with next() → { value, done }
  • Iterable protocol: object with [Symbol.iterator]() → iterator
  • Built-in iterables: Array, String, Map, Set, TypedArray, NodeList, generators
  • for...of: uses [Symbol.iterator]; for...in: enumerates object keys (strings)
  • Generator function*: produces iterators with yield; lazy, stateful, resumable
  • yield*: delegates to another iterable — flattens or chains sequences
  • next(value): send a value INTO a generator — two-way communication
  • Async iteration: async function* + yield; consumed with for await...of
  • Array.from(iterable, mapFn): converts any iterable to array with optional mapping
  • Iterators are stateful/consumed — iterables return fresh iterators each time
💡

How to Answer in an Interview

  • 1.Implement the range iterable from scratch — it's the canonical 10-line demo
  • 2.Distinguish iterator vs iterable explicitly — most devs can't define both clearly
  • 3.The String iteration with emoji (vs .split('')) shows deep Unicode knowledge
  • 4.Redux-Saga as real-world generators usage signals senior-level ecosystem awareness
  • 5.Async generators + for await...of for pagination is a great system design answer
📖 Deep Dive Articles
Modern JavaScript: ES6+ Features Every Developer Must Know13 min read

Practice Questions

No questions tagged to this topic yet.

Tag questions in Admin → Questions by setting the "Topic Page" field to javascript-iterator-interview-questions.

Related Topics

JavaScript Array Interview Questions
Intermediate·8–12 Qs
JavaScript Generators Interview Questions
Advanced·3–5 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz