Generators & Iterators in JavaScript

Why should you care about Generators & Iterators in JavaScript?

Iterators and generators help you process data step by step instead of loading everything at once. They are useful for large datasets, custom iteration logic, and advanced interview discussions.

Advanced topic: Learn functions, arrays, for...of, and Promises first. You can solve most everyday loops without writing a custom iterator. Come back to generators when you need lazy data, pause/resume behavior, or controlled cleanup.

An iterator is an object that lets you get values one at a time. Each iterator keeps track of its current position or state.

An iterator must have a next() method. Each call to next() returns an object with:

  • value - the current value
  • done - true if there are no more values, false if there are more

javascript

// Manually building an iterator
function makeCounter(start, end) {
  let current = start;
  
  return {
    next: function() {
      if (current <= end) {
        return { value: current++, done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const counter = makeCounter(1, 3);

console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }

A generator is useful when values should be produced on demand or when the caller should control when work continues. It is not automatically faster. It can reduce memory use when values are produced and consumed one at a time, but the benefit depends on the data and how it is used.

Choose a generator only when its pause-and-produce behavior helps.
Situation Good choice Why
Small list already in memory Array and for...of Simpler code is easier to read and maintain.
Large range or paginated data Generator Produce one item or page at a time instead of building everything first.
Resource needs cleanup when stopped Generator with try/finally return() can close the generator and run cleanup code.
Network or timer-based async stream Async iterator or a Promise-based API A normal generator is synchronous; it does not wait for Promises.

Practical use case: page through records on demand. The caller decides when to request the next page, so the full dataset does not need to be stored in memory at once.

javascript

function* pages(records, pageSize) {
  for (let start = 0; start < records.length; start += pageSize) {
    yield records.slice(start, start + pageSize);
  }
}

// Assume largeRecords is an existing array containing many records.
const pageStream = pages(largeRecords, 100);
const firstPage = pageStream.next().value;
console.log(firstPage.length); // 100

// Stop when the consumer has enough data.
for (const page of pageStream) {
  savePage(page);
  if (page[0]?.id > 500) break;
}
Beginner test: replace largeRecords with a small array, call next() twice, and inspect value and done. Once that feels clear, try the same generator with for...of.

An iterable is an object that can provide an iterator through Symbol.iterator. An iterator is the object with a next() method that returns { value, done }.

In short, an iterable gives you an iterator. Arrays, strings, Sets, and Maps are built-in iterables.

Built-in iterables: Arrays, Strings, Maps, Sets, NodeLists.

javascript

// Arrays are iterable
let notifications = ["New login", "Payment received", "Order shipped"];
let iterator = notifications[Symbol.iterator](); // Get the iterator

console.log(iterator.next()); // { value: "New login",  done: false }
console.log(iterator.next()); // { value: "Payment received", done: false }
console.log(iterator.next()); // { value: "Order shipped", done: false }
console.log(iterator.next()); // { value: undefined, done: true }

// Strings are also iterable - each character is a value
let greeting = "Hi!";
let strIterator = greeting[Symbol.iterator]();

console.log(strIterator.next()); // { value: "H", done: false }
console.log(strIterator.next()); // { value: "i", done: false }
console.log(strIterator.next()); // { value: "!", done: false }
console.log(strIterator.next()); // { value: undefined, done: true }

You can also make your own iterable by adding [Symbol.iterator]() to any object:

javascript

// Create an object that is iterable
let range = {
  from: 1,
  to: 5,
  
  [Symbol.iterator]() {
    let current = this.from;
    let last = this.to;
    
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

// Now we can loop over it
for (let num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

The for...of loop is the cleanest way to loop over any iterable. It works with arrays, strings, Sets, Maps, and any custom iterable.

for...of gets an iterator from the iterable and repeatedly calls next() until done becomes true.

javascript

// for...of with an array
let colors = ["red", "green", "blue"];

for (let color of colors) {
  console.log(color);
}
// red
// green
// blue

// for...of with a string
let status = "ACTIVE";

for (let char of status) {
  console.log(char);
}
// A, C, T, I, V, E

// for...of with a Set
let uniqueNumbers = new Set([1, 2, 3, 2, 1]);

for (let num of uniqueNumbers) {
  console.log(num);
}
// 1, 2, 3  (duplicates removed by Set)

// for...of with a Map
let alertCounts = new Map([["Login", 3], ["Payment", 7]]);

for (let [type, count] of alertCounts) {
  console.log(type + ": " + count);
}
// Login: 3
// Payment: 7

Important: for...of vs for...in - for...in iterates over object keys, while for...of iterates over values of an iterable.

A generator function can pause at yield and continue later when next() is called. Calling the function returns a generator object; its body does not run until the first next() call.

Generators are useful when:

  • You need to produce a series of values lazily (only when asked)
  • You're working with infinite sequences
  • You want to pause execution and come back later

A generator function declared with function* returns a generator object. That generator object is both an iterator and an iterable, so it has next() and can also be used with for...of.

The yield keyword pauses the generator. If a value follows yield, that value becomes the value returned by next(). The next next() call resumes execution after that yield.

javascript

// A normal function - runs all the way through
function normalGreet() {
  console.log("Hello!");
  console.log("How are you?");
  console.log("Goodbye!");
}

normalGreet(); // Prints all three at once, no stopping

// A generator function - can be paused at each yield
function* greetGenerator() {
  console.log("Hello!");
  yield;  // Pause here
  console.log("How are you?");
  yield;  // Pause here
  console.log("Goodbye!");
}

let gen = greetGenerator(); // Does NOT run yet
gen.next(); // Runs until first yield: prints "Hello!"
gen.next(); // Continues: prints "How are you?"
gen.next(); // Continues to end: prints "Goodbye!"

The * after function declares a generator function. Generator functions use yield to pause and return a value.

Calling a generator function gives you a generator object. Each call to .next() on that object returns { value, done }, just like any other iterator.

javascript

// Generator that yields values
function* notificationFeed() {
  yield "New login";
  yield "Payment received";
  yield "Order shipped";
}

let gen = notificationFeed();

console.log(gen.next()); // { value: "New login",  done: false }
console.log(gen.next()); // { value: "Payment received", done: false }
console.log(gen.next()); // { value: "Order shipped", done: false }
console.log(gen.next()); // { value: undefined, done: true }

// Generators work with for...of
for (let alert of notificationFeed()) {
  console.log(alert); // New login, Payment received, Order shipped
}

// Generator that yields computed values
function* multiplyBy(start, factor, count) {
  for (let i = 0; i < count; i++) {
    yield start * factor;
    start++;
  }
}

for (let val of multiplyBy(1, 3, 5)) {
  console.log(val); // 3, 6, 9, 12, 15
}

A generator is a built-in way to create an iterator. The main difference is that you write yield statements instead of manually building the next() logic yourself.

Advanced: Passing a Value to next()

A value passed to next(value) becomes the result of the yield expression where the generator is paused. The first next() argument is ignored because the generator has not reached a yield yet.

javascript

// next(value) sends a value back into the paused generator
function* askName() {
  const name = yield "What is your name?";
  console.log(name);
}

const interview = askName();
console.log(interview.next().value); // "What is your name?"
interview.next("Sam"); // "Sam"

Generator lifecycle methods are small but useful: next() continues execution, return() ends the generator, and throw() throws an error inside the generator.

javascript

// finally still runs when a generator is closed early
function* readTasks() {
  try {
    yield "task-1";
    yield "task-2";
  } finally {
    console.log("Cleaning up");
  }
}

const tasks = readTasks();
tasks.next();
tasks.return(); // Logs "Cleaning up"

If a generator is closed early with return(), its finally block can run. This makes generators useful when cleanup is required, but cleanup is not automatic for every generator.

An infinite generator is safe to create because it produces values on demand. Your code must still stop consuming it with a condition such as break:

javascript

// An infinite counter - only calculates values on demand
function* infiniteCounter(start = 0) {
  while (true) {
    yield start++;
  }
}

let counter = infiniteCounter(10);

for (let value of counter) {
  console.log(value); // 10, 11, 12
  if (value === 12) break; // Always stop an infinite generator yourself
}

Here are real-world situations where generators shine:

1. Lazy evaluation / large data sets - Produce values only when they are requested. This can avoid creating a large array in memory when values are consumed one at a time.

javascript

// Processing a huge range of numbers without creating the whole array
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

// Only the numbers that pass the filter are processed
for (let num of range(1, 1000000)) {
  if (num % 7 === 0) {
    console.log(num); // First few multiples of 7
    if (num > 50) break; // Stop when done
  }
}
// Compared with creating a million-element array upfront,
// this can use less memory when values are produced and consumed one at a time

2. Infinite sequences - auto-incrementing IDs, timestamps, or any counter with no defined end.

javascript

// Auto-incrementing ID generator
function* createId() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const getId = createId();

const alert1 = { type: "Login",   id: getId.next().value }; // id: 1
const alert2 = { type: "Payment", id: getId.next().value }; // id: 2
const alert3 = { type: "Shipment", id: getId.next().value }; // id: 3

3. Async control flow history - Generators are not inherently asynchronous. Before native async/await, libraries like co used generators to help structure async code.

As an advanced next step, JavaScript also has async function* and for await...of for working with async iterables.

Interview Tip: Generators are a common topic in advanced JavaScript interviews. Understand how yield, next(), iterators, and iterables work.

  • Iterator: An object with a next() method that returns { value, done }. It lets you step through a sequence one item at a time.
  • Iterable: An object that can be iterated, usually through [Symbol.iterator](). Arrays, strings, Sets, and Maps are all iterables.
  • for...of: The easiest way to loop over any iterable. It automatically calls next() until done is true.
  • Generator function: Declared with function*. Calling it returns a generator object, which is both an iterator and an iterable.
  • yield: Pauses the generator and returns a value to the caller. The function resumes from that exact point on the next .next() call.
  • Lazy evaluation: Generators only produce values when asked. This can use less memory when values are produced and consumed one at a time.
  • Infinite sequences: Generators are useful for sequences that have no natural end, but you must stop consumption yourself, often with break.
  • Iterator vs Generator: An iterator is any object with next(). A generator is a convenient way to create one.
  • Generators are iterators: A generator object is both an iterable and an iterator. It has [Symbol.iterator]() and next() built in.

What's next? Head back to the Memory Management tutorial to understand how JavaScript handles memory automatically.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.