JavaScript

What are iterators and generators in JavaScript?

The short answer

An iterator is any object that implements a next() method which returns { value, done }. It's a protocol — a contract for producing a sequence of values one at a time. A generator is a special kind of function (declared with function*) that makes creating iterators easy. Instead of manually building the next() method yourself, you write what looks like normal code with yield statements, and JavaScript handles the rest.

The iterator protocol

At its core, an iterator is simple. It's an object with a next() method. Each call to next() returns an object with two properties:

  • value — the next value in the sequence
  • done — a boolean indicating whether the sequence is finished
function createCountdown(start) {
let current = start;
return {
next() {
if (current > 0) {
return { value: current--, done: false };
}
return { value: undefined, done: true };
},
};
}

const countdown = createCountdown(3);
countdown.next(); // { value: 3, done: false }
countdown.next(); // { value: 2, done: false }
countdown.next(); // { value: 1, done: false }
countdown.next(); // { value: undefined, done: true }

Each call advances the sequence by one step. Once done is true, the iterator is exhausted. This is the foundation that powers for...of loops, spread syntax, destructuring, and many other JavaScript features.

What makes something iterable

An iterator and an iterable are different things. An iterable is any object that has a [Symbol.iterator] method which returns an iterator. This is what for...of looks for.

const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};

for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

When you write for (const x of something), JavaScript calls something[Symbol.iterator]() to get an iterator, then repeatedly calls next() until done is true. That's all for...of does under the hood.

Built-in iterables

Many things you use every day are iterable:

  • Arraysfor (const item of [1, 2, 3])
  • Stringsfor (const char of "hello") iterates over characters (and handles Unicode correctly, unlike indexing)
  • Mapsfor (const [key, value] of myMap)
  • Setsfor (const item of mySet)
  • NodeListsfor (const el of document.querySelectorAll('div'))

Plain objects are not iterable by default. That's why for...of doesn't work on {}. You'd need Object.keys(), Object.values(), or Object.entries() to get an iterable view of an object.

Generator functions

Writing iterators by hand is verbose. You have to manage state, track when you're done, and build the next() method yourself. Generators simplify all of this.

A generator function is declared with function* and uses yield to produce values:

function* countdown(start) {
while (start > 0) {
yield start;
start--;
}
}

const iterator = countdown(3);
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: undefined, done: true }

The magic is that the function pauses at each yield and resumes when you call next() again. All the state (local variables, where you are in the loop) is preserved between calls. The generator handles the { value, done } wrapping for you.

Generators are also automatically iterable, so they work with for...of:

for (const num of countdown(3)) {
console.log(num); // 3, 2, 1
}

yield is a two-way street

Here's where things get interesting. yield doesn't just send values out — it can receive values in. The argument you pass to next() becomes the result of the yield expression:

function* conversation() {
const name = yield 'What is your name?';
const color =
yield `Hello ${name}! What is your favorite color?`;
return `${name} likes ${color}`;
}

const chat = conversation();
chat.next(); // { value: "What is your name?", done: false }
chat.next('Alice'); // { value: "Hello Alice! What is your favorite color?", done: false }
chat.next('blue'); // { value: "Alice likes blue", done: true }

The first next() runs until the first yield and produces the question. The second next('Alice') sends 'Alice' back into the generator as the result of that first yield, then runs until the next yield. This two-way communication is what makes generators powerful for complex flow control.

Lazy evaluation and infinite sequences

One of the most useful properties of generators is that they're lazy — they only compute values when asked. This means you can represent infinite sequences without blowing up memory:

function* naturals() {
let n = 1;
while (true) {
yield n++;
}
}

function* take(n, iterable) {
let count = 0;
for (const value of iterable) {
if (count >= n) return;
yield value;
count++;
}
}

for (const num of take(5, naturals())) {
console.log(num); // 1, 2, 3, 4, 5
}

The naturals() generator produces numbers forever, but because it's lazy, it only generates what take() asks for. No array of numbers is ever created in memory.

yield* for delegation

If you want one generator to yield all the values from another iterable, use yield*:

function* concat(a, b) {
yield* a;
yield* b;
}

for (const num of concat([1, 2], [3, 4])) {
console.log(num); // 1, 2, 3, 4
}

yield* delegates to another iterable, yielding each of its values one by one. It works with any iterable — arrays, strings, other generators, anything with [Symbol.iterator].

Real-world use cases

Pagination. A generator can encapsulate the logic of fetching pages of data:

async function* fetchPages(url) {
let nextUrl = url;
while (nextUrl) {
const response = await fetch(nextUrl);
const data = await response.json();
yield data.results;
nextUrl = data.next;
}
}

Unique ID generation. A simple, stateful sequence without any external counter variable:

function* idGenerator(prefix = 'id') {
let id = 0;
while (true) {
yield `${prefix}_${id++}`;
}
}

const nextId = idGenerator('user');
nextId.next().value; // "user_0"
nextId.next().value; // "user_1"

Tree traversal. Generators are natural for recursive data structures because they can yield values as they walk:

function* traverse(node) {
yield node.value;
for (const child of node.children) {
yield* traverse(child);
}
}

Why interviewers ask this

Iterators and generators sit at the intersection of several important JavaScript concepts — protocols, lazy evaluation, control flow, and the mechanics behind for...of and spread syntax. Understanding them shows you know how the language works beneath the surface, not just its syntax. Senior-level candidates are expected to recognize when a generator is the right tool (complex sequences, pagination, streaming data) versus when a simple array or callback would do.