SKU // 80

JavaScript Iterators

An iterator is the machinery behind every for...of loop. It is an object with one job: each time you call its next() method, it hands you the next value and tells you whether it is finished. Understanding this lets you make your own objects loopable.

// The Iterator Protocol

Every call to next() returns an object with two properties:

valueThe next item
donefalse while there are more items, true when finished

// Driving an Iterator by Hand

Ask an array for its iterator, then call next() yourself:

const colors = ["red", "green"];
const it = colors[Symbol.iterator]();

console.log(it.next());
console.log(it.next());
console.log(it.next());
{value: "red", done: false} {value: "green", done: false} {value: undefined, done: true}

That is exactly what for...of does for you: get the iterator, call next() over and over, run the loop body with each value, and stop when done is true.

// What for...of does, written out by hand
const iter = colors[Symbol.iterator]();
let step = iter.next();
while (!step.done) {
  console.log(step.value);
  step = iter.next();
}

// Iterators Are Used Up

An iterator only moves forward. Once it reaches the end, it stays finished:

const it = new Set([1, 2]).values();

console.log([...it]);   // [1, 2]
console.log([...it]);   // []  already exhausted
[1, 2] []
Watch out
If you store set.values() or map.entries() in a variable and loop over it twice, the second loop sees nothing. Call the method again to get a fresh iterator.

// next() Alone Is Not Enough

Any object with a working next() method is an iterator. Here is a home-made one that counts by tens forever:

function myNumbers() {
  let n = 0;
  return {
    next() {
      n += 10;
      return { value: n, done: false };
    }
  };
}

const n = myNumbers();
console.log(n.next().value, n.next().value, n.next().value);
for (const x of n) { }
10 20 30 TypeError: n is not iterable

Calling next() by hand works, but for...of refuses. It does not look for next(); it looks for a [Symbol.iterator] method that returns an iterator. An object needs both halves to work with for...of, which is exactly what the next example builds.

// Making Your Own Iterable

Give an object a [Symbol.iterator] method that returns an iterator, and suddenly for...of, spread, and Array.from all work on it:

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 { value: undefined, done: true };
      }
    };
  }
};

for (const n of range) console.log(n);
console.log([...range]);
1 2 3 4 5 [1, 2, 3, 4, 5]

Each time a loop starts, it calls [Symbol.iterator]() again and gets a fresh iterator with current back at the start. That is why range can be looped over as many times as you like.

Reading the Code
The square brackets in [Symbol.iterator]() are a computed method name (from Object Properties). Symbol.iterator is a special built-in key that JavaScript looks for; you cannot type it as a plain name.

One more rule: once done is true, value is ignored, so you can leave it out and return just { done: true }. That also means a value sent with done: true is never seen by for...of or spread.

W3Schools errors in this topic
W3Schools' version of this example writes myNumbers = {}; and done = false; with no let or const. Those are automatic globals (JS Variables), and in strict mode (JS Strict Mode) the code crashes with ReferenceError: myNumbers is not defined. It also returns { value: 100, done: true } at the end, so its loop stops at 90: the 100 is thrown away. And it says Symbol.iterator "returns a next() function"; it actually returns an object that has a next() method.

// Iterables vs Iterators

Iterable Iterator
Has[Symbol.iterator]()next()
JobHand out a new iteratorHand out values one by one
ReusableYesNo, used up once
Examplean arrayarray.values()

Think of an iterable as a book and an iterator as a bookmark. Anyone can open the book with a new bookmark; each bookmark only moves forward through the pages.

// Iterator Helpers (New in 2025)

Iterators used to have only next(). To filter or transform one, you had to spread it into an array first. ECMAScript 2025 added iterator helpers: methods like the array ones you already know, built right into iterators. They work in current browsers (Chrome since 2024).

Iterator.from() turns any iterable (or iterator) into an iterator that has the helpers:

const it = Iterator.from([1, 2, 3, 4, 5, 6]);

const result = it
  .filter(x => x % 2 === 0)   // 2, 4, 6
  .map(x => x * 10)            // 20, 40, 60
  .take(2);                     // just the first two

console.log(result.toArray());
[20, 40]

Iterators from Sets, Maps, and array methods like values() already have the helpers, so you can skip Iterator.from() there: new Set(["a", "b"]).values().map(s => s.toUpperCase()).

HelperReturnsExampleResult
map(fn)iterator[1,2,3] map x => x * 22, 4, 6
filter(fn)iterator[32,33,16,40] filter x => x > 1832, 33, 40
flatMap(fn)iterator[1,2,3] flatMap x => [x, x * 10]1, 10, 2, 20, 3, 30
take(n)iterator[1,2,3,4,5,6] take(5)1, 2, 3, 4, 5
drop(n)iterator[1,2,3,4,5,6] drop(5)6
find(fn)value[3,10,18,30,20] find x => x > 1830
some(fn)true / false"123456789" some x => x > 7true
every(fn)true / false"123456789" every x => x > 7false
reduce(fn, start)value[175,50,25] reduce (a, b) => a - b100
forEach(fn)undefinedruns fn once per value
toArray()arraycollects every remaining value

The first five are lazy: they return a new iterator and do no work until something asks for values. That is why take() is safe even on a sequence that never ends (you will build one in the next lesson).

Trap 1: helpers belong to iterators, not arrays
[1, 2, 3].drop(1) throws TypeError: [1,2,3].drop is not a function. Arrays have their own map and filter but no drop or take. Wrap the array first: Iterator.from([1, 2, 3]).drop(1). And because map, filter, take, and friends return an iterator, logging one shows an Iterator object, not your values. Finish with .toArray() or spread it.
Trap 2: helpers use up the iterator
find() stops at the first match, and the iterator stays where it stopped:
const it = Iterator.from([1, 2, 3, 4]);
console.log(it.find(x => x > 1));   // 2
console.log([...it]);                    // [3, 4]: 1 and 2 are gone

W3Schools' drop() example stores myIterator.drop(5) in a variable named firstFive. Its comment is right (it removes the first five), but the name is backwards: what is left is [6].

Coming Next
Writing next() by hand is a lot of code for a simple idea. Generators, in the next lesson, produce iterators with far less typing.

// Quick Check

An object has a working next() method but no [Symbol.iterator]. What happens with for (const x of obj)?

  1. It loops using next()
  2. It throws TypeError: obj is not iterable
  3. It loops over the object's keys
  4. It runs zero times with no error
Show answer
B. for...of looks for [Symbol.iterator], not next(). The object is an iterator but not an iterable.

What does this log?

const it = Iterator.from([5, 10, 15, 20]);
console.log(it.drop(1).map(x => x / 5).toArray());
Show answer
[2, 3, 4]. drop(1) skips the 5, then map divides 10, 15, and 20 by 5.

Why can take(3) be used on a sequence that never ends, when toArray() cannot?

  1. take is faster
  2. take is lazy: it only pulls 3 values, while toArray tries to collect every value
  3. toArray only works on arrays
  4. It cannot; both freeze the page
Show answer
B. take, map, filter, drop, and flatMap do no work until values are requested. toArray() keeps asking until done, which never comes.

// Try It

  1. Click Open Playground. Keep calling next() until done is true
  2. Store map.keys() in a variable, spread it twice, and explain the second result
  3. Open the range example with its Try it Yourself button and loop over it twice
  4. Change range so it counts by twos (1, 3, 5...)
// Video Walkthroughs
Last updated: Oct 10, 2026, 1:42 AM ET