Iterables, Iterators, and Generators: How JavaScript Actually Loops
A conceptual breakdown of iterables, iterators, and generators in JavaScript, showing how they work together to power loops and lazy data processing.
At some point in JavaScript, you start using things like:
for (const x of something) {}or:
const arr = [1, 2, 3]
[...arr]They feel simple. Natural, even.
But behind that simplicity is a system that’s surprisingly elegant — and a bit hidden.
Three concepts sit underneath all of this:
- iterable
- iterator
- generator
At first, they sound confusing because the names are similar. But once you see how they connect, they form a very clean mental model of how JavaScript handles sequences.
The System Behind for...of
When you write:
for (const x of something) {}JavaScript doesn’t treat arrays specially.
Instead, it follows a protocol:
1. Look for something[Symbol.iterator]
2. Call it → get an iterator
3. Repeatedly call next()
4. Stop when done is trueSo looping is not about arrays.
It’s about whether something follows this protocol.
Iterable: The Entry Point
An iterable is any object that has:
Symbol.iteratorThat’s it.
Example:
const arr = [1, 2, 3]
arr[Symbol.iterator] // existsSo arrays are iterable.
But the important idea is this:
Iterable means “this object knows how to start an iteration.”
It doesn’t actually produce values itself. It just provides a way to create something that does.
Iterator: The Thing That Produces Values
When you call:
const iterator = arr[Symbol.iterator]()you get an iterator.
An iterator is an object with one method:
next()Each call to next() returns:
{ value: ..., done: true/false }Example:
const arr = [1, 2, 3]
const iterator = arr[Symbol.iterator]()
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true }The important insight is that an iterator is stateful.
It remembers where it is.
You can think of it as a cursor moving through data:
[1, 2, 3]
↑
cursorEach call to next() moves the cursor forward.
Iterable vs Iterator
This is where confusion usually happens.
- Iterable → has
Symbol.iterator() - Iterator → has
next()
And usually:
iterable → creates iteratorAn array is iterable.
But when you call:
arr[Symbol.iterator]()you get the iterator.
They are related, but not the same thing.
Generators: Making Iterators Easier
Writing iterators manually is possible, but not pleasant.
You have to manage state yourself:
const iterator = {
current: 1,
next() {
if (this.current <= 3) {
return { value: this.current++, done: false }
}
return { done: true }
}
}Generators simplify this.
function* gen() {
yield 1
yield 2
yield 3
}Now:
const g = gen()
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }The yield keyword pauses execution and resumes it later.
So instead of manually tracking state, JavaScript does it for you.
Generators Are Both Iterable and Iterator
This is the elegant part.
A generator returns an object that is:
- an iterator (it has
next()) - also an iterable (it has
Symbol.iterator)
So you can do both:
const g = gen()
g.next() // manual iteration
for (const x of g) {
console.log(x)
}Generators sit right in the middle of the system.
Why This Design Exists
This system allows JavaScript to unify many features:
for...of- spread operator (
...) - destructuring
- async iteration
Instead of hardcoding behavior for arrays, JavaScript says:
“If your object follows this protocol, I will treat it like a sequence.”
This makes the language flexible.
Any object can become iterable if it implements the right method.
Lazy Evaluation and Control
One of the most powerful ideas behind iterators and generators is lazy evaluation.
Instead of producing all values at once:
const arr = [1, 2, 3, ..., 1000000]you can generate them one by one:
function* range(n) {
for (let i = 1; i <= n; i++) {
yield i
}
}Now values are produced only when needed.
This is useful for:
- large datasets
- streaming data
- performance optimization
A Subtle Behavior: Iterators Are Consumed
There’s an important detail that often surprises people.
function* gen() {
yield 1
yield 2
}
const g = gen()
for (const x of g) {
console.log(x)
}
for (const x of g) {
console.log(x)
}The second loop prints nothing.
Why?
Because the iterator has already reached:
done: trueIt does not reset automatically.
This reinforces the idea that an iterator is a stateful process, not just a static collection.
A Better Mental Model
Instead of thinking:
“arrays are iterable, objects are not”
It’s more accurate to think:
Iterable → knows how to start iteration
Iterator → knows how to continue iteration
Generator → easiest way to build that processAnd the deeper idea:
JavaScript doesn’t define behavior by type.
It defines behavior by protocols.
If your object follows the rules, it becomes part of the system.
That’s what makes features like for...of feel simple — even though there’s a surprisingly rich mechanism underneath.