A Complete Guide to ES6 Iterators and Clean Iteration Patterns

February, 9th 2025 4 min read

Iterators in JavaScript are not new, but many developers still only scratch the surface of what they can do. ES6 introduced a unified protocol for sequential data access, making iteration predictable across arrays, maps, sets, strings, and user‑defined structures. When used well, iterators help write clearer and more memory‑efficient code, especially when working with streams, custom data collections, or lazy evaluation.

This updated guide takes a fresh, more practical look at ES6 iterators. The aim is to explain how they work, how to build your own, and how generator functions and asynchronous iterators fit into the larger picture. The examples are modern, direct, and avoid unnecessary abstractions.

1. What Iterators Are and Why They Exist

An iterator is an object designed to produce a sequence of values. It exposes a single method, next(), which returns:

js
{ value: any, done: boolean }

The iteration continues until done becomes true. This approach standardizes how JavaScript structures expose their data and provides the foundation for for...of.

How JavaScript Processes an Iterator

  1. Obtain an iterator by calling [Symbol.iterator]() on an iterable structure.
  2. Call next() to get the first result.
  3. Call next() repeatedly until done becomes true.
  4. Stop iteration automatically when used inside for...of.

This protocol lets any object become iterable simply by implementing Symbol.iterator.

2. Building a Manual Iterator

Manual iterators are uncommon in everyday code, but they help understand how the protocol works.

js
function createIterator(source) {
  let index = 0;
  return {
    next() {
      return index < source.length
        ? { value: source[index++], done: false }
        : { value: undefined, done: true };
    },
  };
}

const it = createIterator(["x", "y", "z"]);

Every call to it.next() produces the next value and moves internal state forward. This is all that an iterator needs to do.

3. Making Custom Objects Iterable

Objects are not iterable by default. Adding Symbol.iterator changes that.

js
const counter = {
  current: 1,
  max: 3,

  [Symbol.iterator]() {
    let value = this.current;
    const limit = this.max;
    return {
      next() {
        return value <= limit
          ? { value: value++, done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const n of counter) {
  console.log(n);
}

Once implemented, the object becomes usable with any language feature that expects an iterable.

4. Using for...of with Iterables

for...of works on any iterable:

js
for (const ch of ["A", "B", "C"]) {
  console.log(ch);
}

This loop form is preferable when you need:

  • Early termination with break.
  • Clean, linear iteration without callback nesting.
  • Natural expression of sequence traversal.

5. Built‑in Iterables and Their Behavior

Many structures in JavaScript implement Symbol.iterator natively:

  • Array
  • String
  • Set
  • Map
  • Typed Arrays
  • arguments
  • DOM NodeList

Iterating Through a Set

js
const s = new Set(["a", "b", "c"]);
for (const item of s) {
  console.log(item);
}

Iterating Through a Map

js
const m = new Map([
  ["id", 1],
  ["role", "admin"],
]);

for (const [key, value] of m) {
  console.log(key, value);
}

Iteration order matches insertion order.

6. Generators: A Better Way to Create Iterators

Generators provide a cleaner way to create iterators without manually writing next().

Basic Example

js
function* sequence() {
  yield "first";
  yield "second";
  yield "third";
}

const gen = sequence();

Generators automatically produce an iterator object and maintain their own state. Each yield pauses execution until the next value is requested.

Why Generators Help

  • Less boilerplate than manual iterator creation.
  • Natural flow control due to pausing and resuming.
  • Useful for infinite sequences, lazy evaluation, and pipelines.

7. Implementing Iterators in Classes

Adding iterator behavior into your own collections can make them much more usable.

js
class List {
  constructor(...items) {
    this.items = items;
  }

  *[Symbol.iterator]() {
    for (const item of this.items) {
      yield item;
    }
  }
}

const list = new List("a", "b", "c");

for (const v of list) {
  console.log(v);
}

This approach creates clean, self-describing iterable data types.

8. Asynchronous Iterators (ES2018)

Some sequences depend on asynchronous operations. JS provides Symbol.asyncIterator for that.

js
const asyncStream = {
  async *[Symbol.asyncIterator]() {
    yield "one";
    yield "two";
    yield "three";
  },
};

(async () => {
  for await (const chunk of asyncStream) {
    console.log(chunk);
  }
})();

Async iterators are ideal for:

  • Streaming responses
  • Reading paginated APIs
  • File system operations (in Node)
  • Real‑time data feeds

9. When Iterators Are Worth Using

Iterators shine when:

  • You want lazy evaluation rather than generating everything upfront.
  • You are building a custom collection type.
  • You must expose a predictable iteration behavior.
  • You are working with large or infinite sequences.
  • You need readable async pipelines.

For small loops or simple data transformations, array methods (map, filter, etc.) remain more convenient.

10. Summary

ES6 iterators unify how JavaScript processes sequences. With explicit iterator objects, generator functions, and their async counterparts, you can build clear, predictable iteration logic tailored to your data structures and performance needs. Understanding iterators deepens understanding of core JavaScript mechanics and gives you better control over how your code processes data.

Mastering them is less about memorizing syntax and more about recognizing where sequential control offers cleaner code than array methods or callbacks.