JavaScript Iterators: How They Work and How to Use Them

JavaScript iterators are the low-level objects that let you step through a sequence of values one item at a time. They power features like for...of, the spread operator, array destructuring, and many built-in data structures.

Quick answer: An iterator is an object with a next() method that returns { value, done }. You usually do not build one manually unless you need custom iteration behavior; most of the time you use iterable objects like arrays, strings, maps, sets, or generator functions.

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript objects, arrays, and how for...of loops read values from a collection.

1. What Is a JavaScript Iterator?

A JavaScript iterator is an object that returns the next value in a sequence each time you ask for it. It follows a simple protocol: call next(), get back an object with a value and a done property, and repeat until done becomes true.

An important distinction is that an iterator is not the same thing as an iterable. An iterable is a value that can produce an iterator, usually through Symbol.iterator.

2. Why JavaScript Iterators Matter

Iterators matter because they give JavaScript a common way to read sequences, whether the data comes from an array, a map, a set, a string, or your own custom type. They make loops and other language features work consistently across many kinds of data.

They are especially useful when you want to:

For most beginners, the biggest benefit is practical: you already use iterators indirectly whenever you loop over modern JavaScript collections.

3. Basic Syntax or Core Idea

The simplest iterator has a next() method. Each call returns an IteratorResult object with the shape { value: ..., done: ... }.

Minimal iterator example

This example shows the core idea without involving any loop syntax yet.

const iterator = {
  next() {
    return { value: 1, done: false };
  }
};

const result = iterator.next();

console.log(result);
// { value: 1, done: false }

This iterator always returns the same result, so it is not very useful yet. Real iterators usually change internal state each time next() is called.

Iterator with internal state

Here the iterator moves through a small sequence and stops when it reaches the end.

const iterator = {
  current: 0,
  next() {
    if (this.current < 3) {
      return { value: this.current++, done: false };
    }

    return { value: undefined, done: true };
  }
};

Each call advances current until the sequence is exhausted. After that, the iterator signals completion with done: true.

4. Step-by-Step Examples

Example 1: Reading values manually

You can call next() yourself to see exactly how iteration works.

const letters = ["a", "b", "c"];
const iterator = letters[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

The array provides an iterator through Symbol.iterator. Each call returns the next array item until the sequence ends.

Example 2: Using an iterator with a for...of loop

Most of the time, you do not call next() yourself. JavaScript does it for you inside for...of.

const numbers = [10, 20, 30];

for (const number of numbers) {
  console.log(number);
}

This works because arrays are iterable, and iterable objects can produce an iterator behind the scenes.

Example 3: Checking the iterator result shape

Iterators always return a result object with the same basic structure, even when they are done.

const iterator = ["x"][Symbol.iterator]();

const first = iterator.next();
const second = iterator.next();

console.log(first);
console.log(second);

The first call returns the value "x". The second call indicates that iteration is finished.

Example 4: A custom iterator for a range

This is a realistic pattern when you want an object to behave like a sequence.

const range = {
  start: 1,
  end: 3,
  [Symbol.iterator]() {
    let current = this.start;

    return {
      next() {
        if (current <= this.end) {
          return { value: current++, done: false };
        }

        return { value: undefined, done: true };
      }
    };
  }
};

for (const value of range) {
  console.log(value);
}

This object becomes iterable because it defines Symbol.iterator and returns an iterator object.

5. Practical Use Cases

Iterators are also useful when you want to separate the idea of how to produce values from how to consume them. That separation makes code more flexible and easier to reuse.

6. Common Mistakes

Mistake 1: Confusing an iterator with an iterable

Beginners often assume any object that “can be looped over” is itself an iterator. In JavaScript, the iterable produces the iterator; the iterator actually returns values.

Problem: This code tries to use next() on an array directly, but arrays do not have a next() method.

const items = ["a", "b"];

console.log(items.next());

Fix: Ask the array for its iterator first by calling Symbol.iterator.

const items = ["a", "b"];
const iterator = items[Symbol.iterator]();

console.log(iterator.next());

The fixed version works because Symbol.iterator returns the iterator object for the array.

Mistake 2: Returning the wrong shape from next()

Iterator results must have the expected { value, done } shape. If done is missing or spelled incorrectly, consuming code may behave unexpectedly.

Problem: This custom iterator returns a plain value instead of an iterator result object, so consumers like for...of cannot use it correctly.

const badIterable = {
  [Symbol.iterator]() {
    return {
      next() {
        return 1;
      }
    };
  }
};

for (const value of badIterable) {
  console.log(value);
}

Fix: Return an object with both value and done.

const goodIterable = {
  [Symbol.iterator]() {
    let count = 0;

    return {
      next() {
        if (count < 1) {
          return { value: count++, done: false };
        }

        return { value: undefined, done: true };
      }
    };
  }
};

The corrected iterator works because consuming code can recognize when the sequence ends.

Mistake 3: Reusing an exhausted iterator

Iterators are stateful and usually one-time-use. Once an iterator is done, calling next() again keeps returning done: true.

Problem: This code expects the same iterator to start over, but iterators do not reset automatically.

const iterator = [1, 2][Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// Still exhausted

Fix: Create a fresh iterator when you need to iterate again, or use the iterable again instead of storing the iterator.

const items = [1, 2];

for const value of items) {
  console.log(value);
}

for (const value of items) {
  console.log(value);
}

The fixed version works because each loop asks the array for a new iterator internally.

7. Best Practices

Practice 1: Use iterables, not raw iterators, in most code

If your goal is simply to loop over data, prefer an iterable like an array, set, or custom iterable object. That gives you reusable iteration instead of a one-time cursor.

const numbers = [1, 2, 3];

for (const number of numbers) {
  console.log(number);
}

This is better than storing a single iterator because the iterable can be consumed multiple times.

Practice 2: Keep iterator state private

Custom iterators are easier to reason about when their moving parts are hidden inside the iterator factory, not exposed as public fields.

const counter = {
  [Symbol.iterator]() {
    let value = 0;

    return {
      next() {
        if (value >= 3) {
          return { value: undefined, done: true };
        }

        return { value: value++, done: false };
      }
    };
  }
};

Private state helps prevent accidental mutation from the outside and keeps the iteration behavior predictable.

Practice 3: Return a fresh iterator each time

If an object is iterable, its Symbol.iterator method should usually create a new iterator for each call.

const bag = {
  items: ["pen", "notebook"],
  [Symbol.iterator]() {
    let index = 0;

    return {
      next() {
        if index < this.items.length) {
          return { value: this.items[index++], done: false };
        }

        return { value: undefined, done: true };
      }
    };
  }
};

Each iteration gets its own cursor, so the object can be looped over more than once.

8. Limitations and Edge Cases

A common error message is TypeError: object is not iterable. This usually means you passed a non-iterable value to something that expects an iterable, such as spread syntax, for...of, or array destructuring.

9. Practical Mini Project

Let’s build a small custom iterable that yields the first few even numbers. This example brings together the iterator protocol, Symbol.iterator, and for...of.

const evenNumbers = {
  limit: 5,
  [Symbol.iterator]() {
    let count = 0;
    let current = 0;

    return {
      next() {
        if count >= this.limit) {
          return { value: undefined, done: true };
        }

        const value = current;
        current += 2;
        count++;

        return { value: value, done: false };
      }
    };
  }
};

for (const number of evenNumbers) {
  console.log(number);
}

This iterable yields 0, 2, 4, 6, 8. It is small, self-contained, and works naturally with any code that expects an iterable.

10. Key Points

11. Practice Exercise

Build a custom iterable that generates the first four squares: 1, 4, 9, 16.

Expected output:

1
4
9
16

Hint: Keep a counter inside the iterator and multiply it by itself before returning each value.

Solution:

const squares = {
  [Symbol.iterator]() {
    let n = 1;

    return {
      next() {
        if n > 4) {
          return { value: undefined, done: true };
        }

        const value = n * n;
        n++;

        return { value: value, done: false };
      }
    };
  }
};

for (const value of squares) {
  console.log(value);
}

12. Final Summary

JavaScript iterators are the mechanism that lets you step through values one at a time. They are simple at the protocol level: call next(), read value, and stop when done becomes true. What makes them powerful is that they sit underneath many everyday language features, including for...of, spread syntax, and destructuring.

In practice, you will usually work with iterables rather than raw iterators. Arrays, strings, maps, and sets already support iteration, and custom iterable objects let you define your own sequences when needed. Once you understand the difference between an iterable and an iterator, the rest of JavaScript’s iteration model becomes much easier to use correctly.

Next, try writing a custom iterable that yields file names, quiz questions, or numbers from a business rule. The more you practice with Symbol.iterator and next(), the more natural iterator-based code will feel.