Iterators vs. Iterables: Sorting Out JavaScript’s Two Protocols

JavaScript’s naming conventions can make the relationship between iterables and iterators harder to grasp than it needs to be. Let’s cut through the terminology.

An iterable is any object — like Array, Set, Map, or a string — that follows the iterable protocol. That protocol requires the object to implement a [Symbol.iterator]() method somewhere in its prototype chain. An iterator, on the other hand, follows the iterator protocol, which defines a standard way to produce a sequence of values.

In practice, that means an iterator must implement the iterator interface: a next() method available on the prototype chain. Each call to next() returns an object with two properties: value, which holds the current element’s value, and done, a Boolean that indicates whether the iterator has advanced beyond the final element — not when it reaches the final element, but only when a subsequent call tries to access past it.

You’ve likely already seen built-in iterators in action. While a Map is itself an iterable, its built-in methods keys(), values(), and entries() all return iterator objects:

const theMap = new Map([ [ "aKey", "A value." ] ]);

console.log( theMap.keys() );
// Result: Map Iterator { constructor: Iterator() }

When you iterate over those results with something like forEach, an iterator behaves indistinguishably from an iterable:

const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys().forEach( thing => {
  console.log( thing );
});
// Result: key

All Iterators Are Iterable

Here’s where the lines blur: every iterator also implements the iterable interface, meaning all iterators are iterable:

const theMap = new Map([ [ "key", "value " ] ]);

theMap.keys()[ Symbol.iterator ];
// Result: function Symbol.iterator()

The confusion deepens when you realize you can use an array — a quintessential iterable, not an iterator — to demonstrate how iterators work. None of the built-in iterables are iterators. But when you call the [Symbol.iterator]() method on an iterable, it returns an iterator object created from that data structure:

const theIterable = [ true, false ];
const theIterator = theIterable[ Symbol.iterator ]();

theIterable;
// Result: Array [ true, false ]

theIterator;
// Result: Array Iterator { constructor: Iterator() }

This works the same for Set, Map, and even strings:

const theIterable = "A string."
const theIterator = theIterable[ Symbol.iterator ]();

theIterator;
// Result: String Iterator { constructor: Iterator() }

This manual process — generating an iterator from an iterable via [Symbol.iterator]() — is exactly what happens internally whenever you loop over an iterable. Every time you iterate an array with for…of, you’re actually stepping through an iterator built from that array.

A cleaner alternative is the built-in Iterator.from() method, which creates an iterator object from any iterable without touching [Symbol.iterator]() directly:

const theIterator = Iterator.from([ true, false ]);

theIterator;
// Result: Array Iterator { constructor: Iterator() }

Stepping Through an Iterator

Each call to next() advances the iterator one step and returns the appropriate result object:

const theIterator = Iterator.from([ 1, 2, 3 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: 3, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }

This gives you a more controlled form of traversal than a standard for loop — you access elements one at a time, as needed. That said, you don’t have to call next() manually; iterators have their own Iterator.forEach method:

const theIterator = Iterator.from([ true, false ]);

theIterator.forEach( element => console.log( element ) );
/* Result:
true
false
*/

Iterables Can Be Revisited; Iterators Can’t

The key difference between the two comes down to state. An iterable is an object that can be iterated over — and, crucially, iterated over again after you’ve finished:

const theIterable = [ 1, 2 ];

theIterable.forEach( el => {
  console.log( el );
});
/* Result:
1
2
*/

theIterable.forEach( el => {
  console.log( el );
});
/* Result:
1
2
*/

An iterator, by contrast, represents a singular act of iteration. It maintains internal state tracking its position. Once you’ve traversed an iterator — whether via next() or forEach — it’s exhausted. There’s no rewinding it:

const theIterator = Iterator.from([ 1, 2 ]);

theIterator.next();
// Result: Object { value: 1, done: false }

theIterator.next();
// Result: Object { value: 2, done: false }

theIterator.next();
// Result: Object { value: undefined, done: true }

theIterator.forEach( el => console.log( el ) );
// Result: undefined

That works neatly when using the iterator constructor’s built-in methods to filter or slice part of an iterator’s contents:

const theIterator = Iterator.from([ "First", "Second", "Third" ]);

// Take the first two values from `theIterator`:
theIterator.take( 2 ).forEach( el => {
  console.log( el );
});
/* Result:
"First"
"Second"
*/

// theIterator now only contains anything left over after the above operation is complete:
theIterator.next();
// Result: Object { value: "Third", done: false }

When the iterator reaches its end, that iteration is complete. The content is spent.

Key Takeaways

  • Iterables implement [Symbol.iterator]() and can be looped over repeatedly.
  • Iterators implement next(), returning { value, done } objects, and are single-use.
  • All built-in iterators are iterable; all built-in iterables can produce iterators via [Symbol.iterator]() or Iterator.from().
  • done: true only appears when accessing beyond the last element, not upon returning it.