Menu
Try in Playground

JavaScript Iterators and Generators: function*, yield, Symbol.iterator

How JavaScript's iterator protocol works, how to make your own objects iterable, and how generator functions make the whole thing painless.

The Iterator Protocol

A lot of JavaScript features — for...of, spread (...), destructuring, Array.from, Promise.all — share a single underlying mechanism: the iterator protocol. Once you understand it, they all look like variations of the same idea.

An iterator is any object with a next() method that returns { value, done }:

index.js
Output
Click Run to see the output here.

Call next() repeatedly. Each call returns the next value and a done flag. When done is true, the sequence is over. That's the whole protocol — four keystrokes and a boolean.

Iterable vs Iterator

There's a second, related concept. An iterable is anything that knows how to produce an iterator. It does that through a method stored under a special key: Symbol.iterator.

index.js
Output
Click Run to see the output here.

Arrays are iterables. Calling numbers[Symbol.iterator]() returns a fresh iterator. Strings, Map, Set, and arguments are all iterables too — and that's why for...of works on all of them.

The split matters: the iterable is the collection, the iterator is the cursor. You can ask an iterable for as many independent cursors as you want.

Why for...of Works

for...of is just sugar over the iterator protocol. Under the hood it calls Symbol.iterator, then next() until done is true:

index.js
Output
Click Run to see the output here.

Spread and destructuring do the same thing — they walk an iterator until it's done:

index.js
Output
Click Run to see the output here.

Any object you build that implements Symbol.iterator gets to play in all of these features for free.

Writing a Custom Iterable

Let's make a range object that yields numbers from start to end:

index.js
Output
Click Run to see the output here.

A few things to notice:

  • [Symbol.iterator]() uses a computed method name. The key is the symbol itself, not the string "Symbol.iterator".
  • Each call to [Symbol.iterator]() returns a brand new iterator with its own current. That's what lets you loop over range twice without it being "used up."
  • The returned iterator only needs next(). That's it.

This works, but it's verbose. There's a much better way.

Enter Generators

A generator function is declared with function* (note the star). Instead of running to completion, it can pause at a yield expression and resume later. Calling one doesn't execute the body — it hands back a generator object that is both an iterator and an iterable:

index.js
Output
Click Run to see the output here.

Each next() runs the body until it hits a yield, suspends, and returns { value, done: false }. When the function finishes, you get { value: undefined, done: true }.

And because generators are iterables, they work with everything from the previous section:

index.js
Output
Click Run to see the output here.

Rewriting range With a Generator

Compare the verbose version above to this:

index.js
Output
Click Run to see the output here.

That's it. The * in front of [Symbol.iterator] makes it a generator method. yield i replaces the whole hand-rolled iterator object. No next, no done, no off-by-one risk — just a normal loop with yield instead of push.

This is why generators exist. They turn "write an iterator" into "write a function that yields."

yield vs return

yield suspends; return ends. You can yield as many times as you want — the generator resumes where it left off:

index.js
Output
Click Run to see the output here.

A return inside a generator shows up as { value: "done", done: true } on the call that finishes it. for...of and spread ignore that returned value — they only consume items where done is false. So don't use return value to smuggle a final item into a loop; it'll be skipped.

Lazy and Infinite Sequences

Generators produce values on demand, one at a time. That means you can represent sequences that would be impossible as arrays:

index.js
Output
Click Run to see the output here.

The loop is literally while (true), and yet the program terminates — because the generator only advances when something asks for the next value. You can take the first N items, stop, and the rest never runs:

index.js
Output
Click Run to see the output here.

take is itself a generator that wraps another one. Composing generators like this is a big part of their appeal — small pieces, each doing one thing.

Delegating with yield*

If a generator needs to yield everything from another iterable, yield* delegates to it:

index.js
Output
Click Run to see the output here.

yield* works with any iterable — arrays, sets, other generators — and forwards each item one at a time. It's the iterator equivalent of spreading.

Async Generators, Briefly

A generator declared async function* can yield values that take time — useful for streaming over an API or reading chunks from a file. You consume it with for await...of:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

This snippet isn't runnable here (it needs a real endpoint), but it's worth knowing the shape exists. Once you've understood regular generators, async generators are the same idea with await sprinkled in.

When to Reach for a Generator

Use one when:

  • The sequence is infinite or could be — IDs, timestamps, retry delays.
  • Producing all values is expensive and the consumer might stop early.
  • You're implementing Symbol.iterator on a custom object. It's almost always shorter than hand-rolling the { next() } object.
  • You want to compose streaming transformations (take, filter, map) without building intermediate arrays.

Reach for a plain array when the data is already in memory and small. Generators aren't free — the machinery that suspends and resumes a function has overhead, and stack traces through generator code can be harder to read.

Next: Symbols

Symbol.iterator is the first symbol most people meet, but it's far from the only one. Symbols are a primitive type designed for exactly this kind of extension point — unique keys that let the language and your own code hook into objects without colliding with ordinary property names. That's the next page.

Frequently Asked Questions

What's the difference between an iterable and an iterator in JavaScript?

An iterable is any object with a Symbol.iterator method that returns an iterator. An iterator is the object that actually produces values — it has a next() method that returns { value, done }. Arrays, strings, Map, and Set are iterables; calling their Symbol.iterator method gives you an iterator you can step through.

What is a generator function in JavaScript?

A function declared with function* that produces values lazily using yield. Calling it doesn't run the body — it returns a generator object that's both an iterator and an iterable. Each next() call runs until the next yield, pauses, and returns the yielded value.

What's the difference between yield and return in a generator?

yield pauses the generator and hands a value back, but the function can resume where it left off on the next next() call. return ends the generator for good — it sets done: true and no further values come out. You can yield many times; you can only meaningfully return once.

When would I use a generator instead of an array?

When the sequence is infinite, expensive to compute, or you only need some of the values. A generator produces items one at a time on demand, so you can represent an endless stream of IDs or paginated API results without materialising everything upfront. If you already have a small fixed array, just use the array.

Learn to code with Coddy

GET STARTED