Mastering JavaScript Iterators and Generators

Hero image for Mastering JavaScript Iterators and Generators. Image by Katja Ano.
Hero image for 'Mastering JavaScript Iterators and Generators.' Image by Katja Ano.

Iteration is everywhere in JavaScript, but we often experience it through convenience methods rather than through the language mechanisms underneath. Arrays, for...of, spread syntax, and many libraries all rely on the same deeper protocols.

That is why iterators and generators are worth understanding directly. After that, a lot of JavaScript starts to feel more coherent, especially when we want lazy evaluation, custom iterable objects, or clearer control over sequence generation.


What an Iterator Actually is

An iterator is an object with a next() method. Each call returns an object with a value and a done flag. That is the basic protocol.

An iterable is slightly different. It is an object that provides an iterator via Symbol.iterator, which is why arrays, strings, maps, and sets work with for...of.

That distinction is more useful than it first sounds. for...of does not care whether the value is an array. It only cares whether the value is iterable. That is why custom iterable objects can slot so neatly into everyday JavaScript syntax.


Why Generators Matter

Writing a custom iterator by hand is possible, but usually clumsy because we have to manage internal state manually. Generator functions make that much easier:

const makeRange = function* (  start: number,  end: number,  step = 1): Generator<number> {  for (let index = start; index < end; index += step) {    yield index;  }};for (const value of makeRange(0, 5)) {  console.log(value);}

This is elegant because the function reads like the sequence it produces. We do not have to simulate a state machine manually, because the language handles that pauseandresume behaviour for us.


Lazy Evaluation is the Real Superpower

Generators produce values on demand. That means we do not have to allocate an entire array up front, which is useful for large or even potentially unbounded sequences.

This matters in two ways. Sometimes it does improve memory behaviour. Just as often, though, it improves the shape of the API. Instead of "build the whole list and then hand it over", we can express "produce values as the consumer asks for them". That is a better fit for streams, tree traversal, paginated data, and longrunning sequence generation.


The Iterable Protocol is Already Everywhere

One reason iterators feel obscure is that most of the time we use them indirectly:

  • for...of consumes an iterator
  • spread syntax consumes an iterator
  • Array.from() consumes an iterator
  • Map and Set expose iterables of their contents

Once that clicks, iterators stop feeling like a niche feature. They are not living off in a corner of the language. They are underneath some of the most ordinary code we write.


Building a Custom Iterable

We can also make our own iterable objects:

type TaskList = {  tasks: string[];  [Symbol.iterator](): Generator<string>;};const backlog: TaskList = {  tasks: ['Design', 'Build', 'Test'],  *[Symbol.iterator]() {    for (const task of this.tasks) {      yield task;    }  },};

This lets backlog work naturally with for...of, spread syntax, and any API that expects an iterable.

That is a very useful design trick when we want an object to feel like a collection without pretending it is literally an array. We keep the richer object shape, but still make iteration pleasant for the consumer.


Generator Delegation and Composition

Another feature worth knowing is yield*, which lets one generator delegate to another:

const makeLabels = function* (): Generator<string> {  yield 'Backlog';  yield* backlog;  yield 'Done';};

This is often a cleaner way to compose sequences than pushing values through several temporary arrays. It keeps the lazy behaviour and makes the intent more obvious.


When Iterators and Generators are Useful

  • modelling sequential data lazily
  • building reusable traversal APIs
  • simplifying stateful iteration logic
  • expressing complex sequence generation more readably

They are not necessary for every loop, of course. Sometimes a plain array method is clearer. The point is not to reach for generators everywhere, but to recognise when they are the right level of abstraction.


Easy Things to Get Wrong

People often assume that generators are mainly about performance. Sometimes they help performance, but their primary value is often architectural clarity around lazy sequences and stateful iteration.

Another easy detail to miss is that iterators are consumable. Once a generator has been exhausted, it does not start over automatically. If we need repeated traversal, we usually need a fresh generator or an iterable object that can create one each time.

It can also be tempting to overuse generators for simple collection work. If all we are doing is mapping five array items once, a generator is probably more abstraction than the problem needs.

If we want to read the deeper language details behind these examples, the references below are worth a look:


Wrapping up

Iterators and generators matter because they reveal how JavaScript models sequences underneath many everyday features. When we understand those protocols, we can build APIs that are lazier, clearer, and more expressive, without losing readability. That makes them far more practical than they first appear.

Key Takeaways

  • Iterators expose next(), whilst iterables expose Symbol.iterator.
  • Generators make custom iteration dramatically easier to write and read.
  • Lazy evaluation is one of the most useful reasons to reach for them.
  • Many everyday language features already depend on the same protocols.

Seen as tools for expressing sequences cleanly, iterators and generators become a natural part of modern JavaScript rather than an obscure language corner.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript