Generators, Iterators, and Symbol.iterator in JavaScript
Master JavaScript generators and iterators -- learn function*, yield, Symbol.iterator, and how to build custom iterable objects.
What you'll learn
- ✓The iterator protocol and Symbol.iterator
- ✓Generator functions with function* and yield
- ✓Building custom iterables for for...of loops
Prerequisites
- •JavaScript functions and objects
- •Understanding of for...of loops
Iterators and generators give you fine-grained control over sequences of values. They power for...of loops, spread syntax, destructuring, and many built-in APIs. Understanding them unlocks patterns for lazy evaluation, infinite sequences, and custom iteration.
The iterator protocol
An iterator is any object with a next() method that returns { value, done }.
function createCountIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
}
const iter = createCountIterator(1, 3);
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: undefined, done: true }
The iterable protocol and Symbol.iterator
An iterable is any object with a [Symbol.iterator]() method that returns an iterator. Arrays, strings, Maps, and Sets are all iterables.
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { done: true };
},
};
},
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]
const [a, b] = range; // a = 1, b = 2
Any object that implements [Symbol.iterator] works with for...of, spread, destructuring, Array.from(), and more.
Generator functions
Generators make creating iterators dramatically simpler. A generator function (declared with function*) automatically returns an iterator. The yield keyword pauses execution and produces a value.
function* countUp(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const gen = countUp(1, 5);
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
// Generators are iterable
for (const n of countUp(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
Compare this to the manual iterator above. The generator version is far more readable.
Infinite sequences
Because generators are lazy (they only compute the next value when asked), you can represent infinite sequences.
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take the first 10 Fibonacci numbers
function take(iterable, count) {
const result = [];
for (const value of iterable) {
result.push(value);
if (result.length >= count) break;
}
return result;
}
console.log(take(fibonacci(), 10));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Passing values into generators
yield is a two-way channel. You can pass values back into the generator via next(value).
function* conversation() {
const name = yield "What is your name?";
const age = yield `Hello ${name}! How old are you?`;
return `${name} is ${age} years old.`;
}
const chat = conversation();
console.log(chat.next()); // { value: "What is your name?", done: false }
console.log(chat.next("Alice")); // { value: "Hello Alice! How old are you?", done: false }
console.log(chat.next(30)); // { value: "Alice is 30 years old.", done: true }
The value passed to next() becomes the result of the yield expression inside the generator.
yield* for delegation
yield* delegates to another iterable or generator.
function* innerSequence() {
yield "a";
yield "b";
}
function* outerSequence() {
yield 1;
yield* innerSequence(); // delegates to innerSequence
yield 2;
}
console.log([...outerSequence()]); // [1, "a", "b", 2]
This is powerful for composing generators together.
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // recursive delegation
} else {
yield item;
}
}
}
console.log([...flatten([1, [2, [3, 4]], 5])]);
// [1, 2, 3, 4, 5]
Making a class iterable
You can add [Symbol.iterator] to any class using a generator method.
class Playlist {
#songs = [];
add(song) {
this.#songs.push(song);
return this;
}
*[Symbol.iterator]() {
for (const song of this.#songs) {
yield song;
}
}
}
const playlist = new Playlist()
.add("Song A")
.add("Song B")
.add("Song C");
for (const song of playlist) {
console.log(song); // "Song A", "Song B", "Song C"
}
console.log([...playlist]); // ["Song A", "Song B", "Song C"]
Async generators
When combined with async/await, generators can yield promises — producing async iterables consumed with for await...of.
async function* fetchPages(baseUrl, totalPages) {
for (let page = 1; page <= totalPages; page++) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data;
}
}
async function processAllPages() {
for await (const pageData of fetchPages("/api/users", 5)) {
console.log(`Got ${pageData.results.length} users`);
}
}
Async generators are ideal for paginated APIs, streaming data, or any sequence of asynchronous values.
Practical example: ID generator
function* idGenerator(prefix = "id") {
let counter = 0;
while (true) {
yield `${prefix}_${++counter}`;
}
}
const userId = idGenerator("user");
const orderId = idGenerator("order");
console.log(userId.next().value); // "user_1"
console.log(userId.next().value); // "user_2"
console.log(orderId.next().value); // "order_1"
Generator control: return and throw
You can force a generator to finish with return() or inject an error with throw().
function* controlled() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("Cleanup!");
}
}
const gen2 = controlled();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.return(99)); // "Cleanup!" -> { value: 99, done: true }
console.log(gen2.next()); // { value: undefined, done: true }
The finally block runs when the generator is returned or thrown into, making it useful for cleanup.
Summary
- Iterators are objects with
next()returning{ value, done }. - Iterables implement
[Symbol.iterator]()and work withfor...of, spread, and destructuring. - Generators (
function*+yield) create iterators with minimal boilerplate. - Use
yield*to delegate to other generators or iterables. - Generators support two-way communication via
next(value). - Async generators (
async function*+yield) produce async iterables for streaming data.
Generators and iterators are one of JavaScript’s most underused features. Once you internalize the lazy evaluation model, you will find them invaluable for processing large datasets, building custom sequences, and composing asynchronous workflows.
Related articles
- JavaScript JavaScript Symbols and Well-Known Symbols
Master JavaScript Symbols for unique property keys, explore well-known symbols like Symbol.iterator and Symbol.toPrimitive, and learn metaprogramming patterns.
- JavaScript Iterators and Generators in JavaScript
A deep dive into JavaScript iterators and generators: the iterator protocol, Symbol.iterator, generator functions, yield, async generators, and practical patterns for lazy evaluation and data pipelines.
- JavaScript Destructuring, Spread, and Rest in JavaScript
A complete tour of the ES6 patterns that show up everywhere — object and array destructuring with defaults and renaming, the rest pattern, and spread for copies and merges.
- Node.js Node.js Async Iterators Tutorial
Master async iterators in Node.js for streaming files, paginated APIs, and backpressure-aware data processing.