A Complete Guide to ES6 Iterators and Clean Iteration Patterns
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:
{ 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
- Obtain an iterator by calling
[Symbol.iterator]()on an iterable structure. - Call
next()to get the first result. - Call
next()repeatedly untildonebecomes true. - 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.
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.
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:
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
const s = new Set(["a", "b", "c"]);
for (const item of s) {
console.log(item);
}Iterating Through a Map
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
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.
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.
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.