What are generators and iterators in JavaScript?

Advanced18 min interview
Skills tested:
Iterator protocol: next() method returning { value, done }Symbol.iterator and making custom objects iterableGenerator function* syntax and yield behaviorLazy evaluation: values produced on demand rather than all at oncePassing values back into a generator with next(value) and generator return

Advertisement

🧩 Scenario

In a real codebase, generators appear when you need to produce values lazily (without computing all of them upfront), implement custom iteration over complex data structures, or create infinite sequences where the consumer pulls values on demand. They also underlie async generators, which are used for streaming data processing and paginated API consumption.

Architecture Walkthrough

The Iterator Protocol

An iterator is any object that implements a next() method returning { value, done }. value is the current value in the sequence. done is true when the sequence is exhausted, false otherwise. This is called the iterator protocol, and any object that follows it can be consumed by for...of, spread syntax, destructuring, and Array.from.

An iterable is a broader concept: an object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, Sets, and generator objects are all iterables. You can make any custom class iterable by adding a [Symbol.iterator] method that returns an object with a next() method.

Generator Functions and yield

A generator function is declared with function* and uses yield to produce values. Calling a generator function does not execute any of its body; it returns a generator object. Execution begins only when next() is called on the generator object. Each next() call runs the function body until the next yield, pauses, and returns { value: yieldedValue, done: false }. When the function returns (or falls off the end), next() returns { value: undefined, done: true }.

This pause-and-resume mechanism makes generators fundamentally different from regular functions. The call stack frame for the generator is preserved across next() calls. Local variables retain their values between yields.

Lazy Evaluation and Infinite Sequences

Because a generator only produces the next value when next() is called, it can represent infinite sequences without consuming infinite memory. The generator holds its state and computes the next value only when the consumer requests it. This is lazy evaluation: values on demand rather than all at once.

You can also pass a value back into the generator by calling next(value). That value becomes the result of the yield expression that paused execution inside the generator. This two-way communication channel is the foundation of more advanced patterns like coroutines.


Key Code Explained

// Manual iterator following the protocol
function makeRange(start, end) {
  let current = start;
  return {
    next() {
      if (current <= end) return { value: current++, done: false };
      return { value: undefined, done: true };
    },
  };
}

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

// Generator version: same behavior, much less code
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}
[...range(1, 5)]; // [1, 2, 3, 4, 5]

// Infinite sequence: safe because values are pulled on demand
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
fib.next().value; // 0
fib.next().value; // 1
fib.next().value; // 1
fib.next().value; // 2

// Making a custom class iterable with Symbol.iterator
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  [Symbol.iterator]() {
    return range(this.start, this.end); // reuse the generator above
  }
}

for (const n of new Range(1, 4)) {
  console.log(n); // 1, 2, 3, 4
}

// Two-way communication: passing values in via next(value)
function* logger() {
  const first = yield 'Ready for first input';
  const second = yield `Got: ${first}. Ready for second`;
  return `Done: ${first}, ${second}`;
}
const gen = logger();
gen.next();            // { value: 'Ready for first input', done: false }
gen.next('alpha');     // { value: 'Got: alpha. Ready for second', done: false }
gen.next('beta');      // { value: 'Done: alpha, beta', done: true }

The infinite fibonacci generator is the clearest demonstration of lazy evaluation. The while (true) loop runs forever in theory, but the generator never produces the next value until the consumer calls next(). The consumer controls termination by simply stopping to call next().


Tradeoffs

ApproachProCon
Generator function*Lazy, stateful, infinite sequences possibleLess familiar syntax, debugging paused generators is tricky
Manual iterator objectExplicit, no generator overheadVerbose for complex sequences
Array + map/filter/reduceFamiliar, well-optimized, easy to debugEagerly evaluated: all values computed upfront

What Interviewers Actually Check

  • Whether you know the difference between an iterator and an iterable
  • Whether you know that calling a generator function returns a generator object and does not run the body
  • Whether you can explain what yield does to execution flow
  • Whether you can produce an infinite sequence without running out of memory
  • Whether you know how Symbol.iterator connects a custom class to for...of

Follow-Up Questions

  1. What is an async generator (async function*) and how does it differ from a regular generator?
  2. How does yield* work and how would you use it to delegate to another generator?
  3. What does calling .return(value) or .throw(error) on a generator object do?
  4. How would you implement a lazy map and filter using generators to avoid building intermediate arrays?
  5. How are generators related to how JavaScript async/await worked before native async/await was added?

Common Candidate Mistakes

  • Calling a generator function and expecting it to execute immediately when it only returns a generator object that has not yet run
  • Not knowing that yield suspends execution and that local variables are preserved between calls
  • Forgetting that the generator object is itself both an iterator and an iterable (it has both next() and [Symbol.iterator]())
  • Not knowing that you can pass a value into next(value) to communicate back into the generator
  • Using a generator where a plain array method would be clearer and equally efficient for finite, small datasets

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain the iterator protocol and write a manual iterator object?
  • Can you write a generator function using function* and yield?
  • Can you explain what happens on each next() call including the { value, done } shape?
  • Can you use a generator to produce an infinite sequence safely?
  • Can you implement Symbol.iterator to make a custom object work with for...of?

Summary

The iterator protocol defines any object with a next() method that returns { value, done } as an iterator. An iterable is an object with a [Symbol.iterator]() method that returns an iterator. Built-in iterables include arrays, strings, Maps, Sets, and generator objects. Custom classes become iterable by implementing [Symbol.iterator].

Generators are functions declared with function* that produce iterators automatically. Each yield suspends the function and returns a value to the caller. The next next() call resumes from where the function paused, with all local state intact. This makes generators the natural tool for lazy sequences, infinite streams, and stateful iteration.

The key design insight is that generators invert control: instead of the producer computing all values and handing them over, the consumer pulls each value on demand by calling next(). This means an infinite generator never consumes infinite memory, because it only holds the state needed to compute the next single value.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Do I always need generators for iteration?

No. Arrays, strings, Maps, and Sets are already iterable. Generators are useful when you need lazy or infinite sequences.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement