completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

The iteration protocols

Learn the Symbol.iterator and next() protocol behind for...of, spread, Array.from, destructuring, Map, Set, strings, and your own custom iterable objects.

By the end, you can
  • 01
    Explain the agreementSay how [Symbol.iterator]() makes an iterator, and what each next() call gives back.
  • 02
    Predict consumersFollow how for...of, spread, destructuring, and new Map read values from an iterable.
  • 03
    Build and fix iterablesWrite your own range and fix the error TypeError: object is not iterable.

One agreement behind many tools

In the for…of & for…in lesson, you used for...of to get the values out of arrays and strings. This lesson shows how for...of gets those values. It works with two kinds of objects:

  • An iterable is a value that can make an iterator. Arrays and strings are iterables.
  • An iterator is an object that gives out values one at a time. It remembers which value comes next.

The rules for these two objects are called the iteration protocols. A protocol is an agreement: if an object has the right methods, JavaScript knows how to use it.

Many tools follow this one agreement. Each of these can take values from any iterable:

  • for...of loops
  • spread, like [...values]
  • Array.from(values)
  • array destructuring, like const [a, b] = values
  • Promise.all(values)
  • new Map(values)
Real-life analogyIterable as a vending machine

You ask the machine for a dispenser. Then you press the dispenser once for each snack. The machine can give another customer a new dispenser. Each dispenser remembers how many snacks it has given.

In real life: A vending machine
In JavaScript: An iterable object
In real life: The button that hands you a dispenser
In JavaScript: The [Symbol.iterator]() method
In real life: The dispenser
In JavaScript: The iterator object
In real life: Pressing once to get one snack
In JavaScript: Calling next() to get one { value, done } result

The table below compares the two kinds of objects. Read each row as: "this kind of object has this method, and the method gives back this."

What iterables and iterators have
Kind of objectWhich method must it have?What does the method give back?Examples
IterableIt has a [Symbol.iterator]() method.It gives back a new iterator.Arrays, strings, Maps, Sets, and this lesson's range.
IteratorIt has a next() method.Each call gives back one result, like { value, done }.The object that array[Symbol.iterator]() gives back.
Iterable iteratorIt has both methods.Its [Symbol.iterator]() method gives back the iterator itself.The iterators that arrays, strings, Maps, and Sets make.

The two words sound almost the same, so read them carefully. An array is an iterable. It is not an iterator. The object that array[Symbol.iterator]() gives back is the iterator.

The array colors holds a [Symbol.iterator]() method. An arrow shows it makes an iterator, a box that holds a next() method. Another arrow shows next() gives one result: value red, done false.[Symbol.iterator]()colorsiterablemakesnext()iteratorremembers its placegivesvalue: "red"done: falseone result
The array colors holds "red", "green", and "blue". Its [Symbol.iterator]() method makes an iterator, and the first next() call on that iterator gives { value: "red", done: false }.

Symbol.iterator: the method every loop looks for

FOUNDATION

An iterable must have one method. The method's name is not a normal string. It is the built-in symbol Symbol.iterator. A symbol is a special value that can be used as a property name. It never clashes with a normal name like "iterator". You met symbols in the Symbols lesson.

To use a symbol as a method name, put it in square brackets: [Symbol.iterator]() { ... }. Here are the smallest iterable and iterator:

The smallest iterable and iteratorPop out in the code editor (opens in a new tab)JavaScript
const iterable = {  [Symbol.iterator]() {    return iterator;  },}; const iterator = {  next() {    return { done: true };  },};
  • Lines 2–3: the iterable's [Symbol.iterator]() method gives back the iterator.
  • Lines 8–9: the iterator's next() method gives back a result. This one says done: true right away, so it gives no values at all.

Here is the same shape doing useful work. range(2, 4) gives the numbers 2, 3, and 4.

A small range you can use againPop out in the code editor (opens in a new tab)JavaScript
function range(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          return current <= end
            ? { value: current++, done: false }
            : { done: true };
        },
      };
    },
  };
}

console.log([...range(2, 4)].join(","));

Line 4 makes a variable, current, inside the [Symbol.iterator]() method. The next() method can still use current after that method has finished. (A function that keeps using a variable from around it is called a closure.) So the iterator remembers its place between calls. Each next() call on lines 7–9 does one of two things:

  • If current is not past end, it gives back { value: current, done: false }. Then current++ adds 1 to current.
  • Otherwise, it gives back { done: true }.

On the last line, spread (...) asks the range for an iterator. It calls next() until it sees done: true. The output is 2,3,4.

You can spread the same range again and get the same three numbers. Each call to [Symbol.iterator]() makes a new current that starts at start. This matters when two loops need to start from the beginning.

Most iterables work this way: they make a new iterator each time you ask. The iterable can be used many times. Each iterator it makes goes through the values only once.

No special class needed

JavaScript does not need a special class or parent object. If an object has a [Symbol.iterator] method that gives back an object with a next() method, every loop and tool in this lesson can use it.

JavaScript checks two things when a loop or spread starts:

  • [Symbol.iterator]() must give back an object.
  • That object must have a next() method.

If either check fails, JavaScript throws a TypeError. Below, the method gives back the number 1 instead of an iterator.

A number is not an iteratorPop out in the code editor (opens in a new tab)JavaScript
const broken = {
  [Symbol.iterator]() {
    return 1;
  },
};

console.log([...broken]);

Spread calls the method on line 2 and gets 1. A number is not an object, so spread throws a TypeError before it reads any value.

next() and done: what each call gives back

STEP THROUGH

Every next() call gives back a small object called a result. A result has two properties:

  • value: the value being handed out.
  • done: false while there are values, and true when there are no more.

Code that reads the values stops as soon as it sees done: true. Step through the code below. Line 1 makes a range from 1 to 5. Line 2 asks it for an iterator. Lines 4–7 call next() four times.

Step through a range iterable
Step 0 of 10Ready
Your turn: follow the blue line

Predict each result before you step. Then change the step to make the same range count in a different way.

Running in
  1. script
Next: line 1
const iterator = numbers[Symbol.iterator](); console.log(iterator.next());console.log(iterator.next());console.log(iterator.next());console.log(iterator.next()); function range(start, end, step = 1) {  return {    [Symbol.iterator]() {      let current = start;      return {        next() {          if (current > end) return { done: true };          const value = current;          current = current + step;          return { value, done: false };        },      };    },  };}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Change line 1: how much should each next() call add to current?

Changing the step starts a new run. Predict each result before you step.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Look at the last result. for...of checks done first. If done is true, the loop body does not run. Even if that last result has a value, for...of ignores it.

Call next() yourself
Call next() by handPop out in the code editor (opens in a new tab)JavaScript
const source = "A😀B";const iterator = source[Symbol.iterator](); iterator.next(); // { value: "A", done: false }iterator.next(); // { value: "😀", done: false }iterator.next(); // { value: "B", done: false }iterator.next(); // { value: undefined, done: true }
Results0 calls

No calls yet.

Step 0 of 4string

Pick an iterable. Then press Call next() to get one result at a time.

You call next() by hand on an array, a string, a Map, a Set, or the lesson's range.

A countdown shows the difference between the last value and the final result. The first three calls give 3, 2, and 1. The fourth call gives { value: "go", done: true }. Lines 9–11 give the countdown a [Symbol.iterator]() method that gives back the countdown itself, so spread can use it.

A countdown with a final done resultPop out in the code editor (opens in a new tab)JavaScript
function countdown(start) {
  let current = start;
  return {
    next() {
      return current > 0
        ? { value: current--, done: false }
        : { value: "go", done: true };
    },
    [Symbol.iterator]() {
      return this;
    },
  };
}

console.log([...countdown(3)].join(","));

The output is 3,2,1. A result with done: true may carry a value, but spread, for...of, and the other tools in this lesson ignore it.

The four results from countdown(3). The first three have the values 3, 2, and 1 with done false, and spread keeps them as 3,2,1. The fourth has the value go with done true, so spread stops and ignores go.[...countdown(3)]321done: falsedone: falsedone: falsekept3,2,1"go"done: truespread stops here"go" is ignored
Spread calls next() four times. It keeps the three values that come with done: false and stops at done: true, so "go" never reaches the output.
Strings give whole characters

The Unicode & string internals lesson explains why "😀".length is 2: the emoji is stored in two pieces. The string's iterator does not split it. It gives the emoji as one value.

Consumers: who keeps calling next()?

INTERACTIVE

A consumer is any tool that takes an iterable and reads values from it. Consumers look different in code, but they all follow the same steps:

  1. Call [Symbol.iterator]() to get an iterator.
  2. Call next() again and again.
  3. Stop when they have enough values, or when a result says done: true.
Real-life analogyCustomers pressing the dispenser

Some customers keep pressing until the dispenser is empty. Some take a few snacks and leave. A polite customer who leaves early hands the dispenser back, so it can be tidied up.

In real life: A customer keeps pressing until the dispenser is empty
In JavaScript: for...of, spread, and Array.from read until done: true
In real life: A customer needs only two snacks and leaves
In JavaScript: Destructuring [a, b] = iterable stops early
In real life: A polite customer hands the dispenser back
In JavaScript: Stopping early calls return(), if the iterator has it

The lab below gives each consumer the values 1, 2, and 3. new Map gets two pairs instead, ["a", 1] and ["b", 2]. The lab counts how many times each consumer calls next() and return().

Watch consumers call next()
The counting iterablePop out in the code editor (opens in a new tab)JavaScript
function counting(limit) {  let nextCalls = 0;  let returnCalls = 0;  return {    stats: () => ({ nextCalls, returnCalls }),    [Symbol.iterator]() {      let value = 1;      return {        next() {          nextCalls = nextCalls + 1;          return value <= limit            ? { value: value++, done: false }            : { done: true };        },        return() {          returnCalls = returnCalls + 1;          return { done: true };        },      };    },  };}
Consumer resultdestructuring
Result1,2
next() calls2
return() calls1
Try it yourself

destructuring gave 1,2. It called next() 2 time(s) and return() 1 time(s).

Every count comes from a real iterable whose next() and return() methods count their calls.

An iterator may also have a return() method. JavaScript calls it when a consumer stops early, so the iterator can clean up. For example, it could close a file it was reading. Below, the loop stops after the first value with break.

Stopping early calls return()Pop out in the code editor (opens in a new tab)JavaScript
const stream = {
  [Symbol.iterator]() {
    let value = 1;
    return {
      next() {
        return value <= 3 ? { value: value++, done: false } : { done: true };
      },
      return() {
        console.log("cleaned up");
        return { done: true };
      },
    };
  },
};

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

The output is 1, then cleaned up. The break leaves the loop while values are still left, so JavaScript calls return().

Destructuring is the most surprising consumer. const [a, b] = iterable needs only two values. After the second value, the iterator is not done yet. If it has a return() method, JavaScript calls it. A break or a thrown error inside for...of also calls return().

const [a, b] = source. Box a gets 1 from the first next() call. Box b gets 2 from the second next() call. The value 3 is never read. The iterator's return() method is called once.const [a, b] = source;1a1st next()2b2nd next()3never readreturn()called once
The iterable can give 1, 2, and 3, but [a, b] needs only two values. After two next() calls, JavaScript calls return() to close the iterator, so 3 is never read.

Iterables vs iterators: use again, or use once

INTERACTIVE
Real-life analogyA one-shot iterator is a roll of tickets

Once you tear a ticket off a roll, it is gone from that roll. To start again, you don't tape the tickets back on. You ask the booth for a new roll.

In real life: A roll of tickets
In JavaScript: One iterator object
In real life: Tearing off ticket 1, then ticket 2
In JavaScript: Each next() call moves the saved place forward
In real life: An empty roll stays empty
In JavaScript: A finished iterator does not go back to the start
In real life: Ask the booth for a new roll
In JavaScript: Call the iterable’s [Symbol.iterator]() again

The lab below calls next() four times on one array iterator. The array has only two items, so the last two calls give done: true. Then line 9 asks the array for a new iterator.

Use the iterator again, or ask for a new one?
A used-up iterator and a new onePop out in the code editor (opens in a new tab)JavaScript
const iterable = ["tea", "cake"];const iterator = iterable[Symbol.iterator](); console.log(iterator.next());console.log(iterator.next());console.log(iterator.next());console.log(iterator.next()); const fresh = iterable[Symbol.iterator]();console.log(fresh.next());console.log(iterator[Symbol.iterator]() === iterator);
Outputwaiting

Not run yet.

Try it yourself

Run the code. Then compare the first iterator with the new one.

The array can be used again. Each iterator it makes is used only once.
The array iterable holds tea and cake. On line 2 it makes the box iterator, which is used up and now gives done true. On line 9 it makes the box fresh, whose first result is value tea.done: trueiteratorused upline 2teacakeiterableline 9value: "tea"freshstarts over
The iterator from line 2 is used up and keeps giving done: true. Line 9 asks the same array again, and the fresh iterator starts over at "tea".

A consumer can also start in the middle of an iterator. Line 4 takes the first color. Line 5 spreads the same iterator, so it gets only the colors that are left.

Spread starts where the iterator isPop out in the code editor (opens in a new tab)JavaScript
const colors = ["red", "green", "blue"];
const iterator = colors[Symbol.iterator]();

console.log(iterator.next().value);
console.log([...iterator].join(","));

The output is red, then green,blue. Spread does not go back to the array. It reads from the iterator it was given, starting where that iterator is.

This works because the iterators JavaScript makes are iterables too. Their [Symbol.iterator]() method gives back the iterator itself, so iterator[Symbol.iterator]() === iterator is true. You can pass such an iterator to any consumer. The consumer starts from the iterator's current place, not from the beginning.

Built-in iterables you already use

Many values in JavaScript are already iterables:

  • arrays
  • strings
  • Maps and Sets
  • typed arrays, which are arrays of raw numbers, like Uint8Array
  • arguments, the list of arguments inside a normal (non-arrow) function
  • in browsers, lists of page elements, like a NodeList

The table below shows what each one gives you, one value for each next() call.

What built-in iterables give you
Which iterable?What does each next() give?Example code
ArrayEach item, in order.for (const item of items)
StringEach character. An emoji counts as one.[..."A😀B"]
MapEach [key, value] pair, in the order it was added.for (const [key, value] of map)
SetEach value, in the order it was added.new Set(array) and for...of
Typed arrayEach number stored in it.The values of a Uint8Array
NodeList (browser)Each element, in page order.document.querySelectorAll(...)

A Map gives one pair for each entry: a two-item array, [key, value]. So you can split each pair right in the loop with destructuring: for (const [key, value] of map). You met Map and Set in the Map & Set lesson. This agreement is what lets them work with for...of and spread.

A Map gives pairsPop out in the code editor (opens in a new tab)JavaScript
const settings = new Map([
  ["theme", "dark"],
  ["font", "mono"],
]);

for (const [key, value] of settings) {
  console.log(key + "=" + value);
}

The output is theme=dark, then font=mono. The Map keeps the order the entries were added. Each entry comes out as a two-item array, so line 6 can split it into key and value.

Plain objects are not iterable

INTERACTIVE

A plain object, like { a: 1, b: 2 }, has no [Symbol.iterator]() method. JavaScript can't guess which values you want: the keys, the values, or both. So for...of on a plain object throws a TypeError. You can fix it in two ways:

  • Turn the object into an array first, for example with Object.entries.
  • Give the object its own [Symbol.iterator]() method, when its values have a clear order.
Fix TypeError: object is not iterable
The failing loopPop out in the code editor (opens in a new tab)JavaScript
const plain = { a: 1, b: 2 }; for (const item of plain) {  console.log(item);} for (const entry of Object.entries(plain)) {  console.log(entry);}
Resulterror
Press Try plain object to run the failing loop in your browser.
Try it yourself

Your browser throws a real TypeError, because the plain object has no [Symbol.iterator]() method.

A plain object is not a list of values. Turn it into an array, or give it its own iterator.

Most of the time, Object.entries(object) is the simple fix. It gives back an array of [key, value] pairs, and arrays are iterable. You saw it in the Object.keys, values & entries and Destructuring lessons.

Turn an object into pairsPop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha", role: "editor" };

for (const [key, value] of Object.entries(user)) {
  console.log(key + "=" + value);
}

The output is name=Asha, then role=editor. Pick the method that gives you what you need:

  • Object.keys for the keys.
  • Object.values for the values.
  • Object.entries for both, as pairs.

Where you’ll use this

Knowing the agreement helps you write helpers that accept more kinds of input. A helper that only asks for an iterable works with an array, a Set, a Map's keys, or a custom range.

It also helps you read errors. When spread, destructuring, Promise.all, or new Map says a value "is not iterable", it means it could not find a [Symbol.iterator]() method on that value.

The helper below takes the first two values from any iterable. Line 2 asks for an iterator. Lines 3–4 call next() twice. Line 8 passes it a Set, and it gives back draft and review.

A helper that accepts any iterablePop out in the code editor (opens in a new tab)JavaScript
function firstTwoLabels(items) {  const iterator = items[Symbol.iterator]();  const first = iterator.next();  const second = iterator.next();  return [first.value, second.value];} console.log(firstTwoLabels(new Set(["draft", "review", "done"])));

A class can follow the agreement too. Below, the constructor saves an array in this.items. The [Symbol.iterator]() method passes the job to that array: it gives back the array's own iterator. So the class needs no next() method of its own.

Make a shopping-list class iterablePop out in the code editor (opens in a new tab)JavaScript
class ShoppingList {
  constructor(items) {
    this.items = items;
  }

  [Symbol.iterator]() {
    return this.items[Symbol.iterator]();
  }
}

const groceries = new ShoppingList(["tea", "rice"]);
console.log([...groceries].join(","));

The output is tea,rice. You can spread groceries again, because every call to [Symbol.iterator]() makes a new array iterator. Use this for a collection whose items have a clear order. Don't make a plain object iterable just to avoid choosing between its keys and its values.

Iterable, iterator, both, or neither?
  • ["red", "green"]
  • ["red"][Symbol.iterator]()
  • { name: "Ada" }
  • { next() { return { done: true }; } }
  • "A😀B"
  • new Map([["a", 1]])
  • range(1, 3)
  • new Set([1, 2]).values()
Try it yourself
0 of 8 correct

Sort each value. Ask two questions: does it have a [Symbol.iterator]() method? Does it have a next() method?

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Iterator helpers come later

The Iterator helpers lesson comes after the Generators lesson. Iterator helpers are methods like map and filter for iterators. They are built on the agreement you learned here.

Common misconceptions

“Anything with values is iterable.”

No. for...of looks for a [Symbol.iterator]() method. A plain object has values but no such method, so it is not iterable.

“An iterable and an iterator are the same thing.”

An iterable makes iterators. An iterator has next() and remembers its place. Some objects are both, such as the iterators that arrays and strings make.

“The final value always matters.”

Once done is true, for...of stops. It ignores any value on that last result.

“Iterators go back to the start after they finish.”

A finished iterator stays finished. To start again, ask the iterable for a new iterator.

“Stopping early just leaves the iterator behind.”

If the iterator has a return() method, JavaScript calls it when a consumer stops early. That gives the iterator a chance to clean up.

Practice: trace and build iterables

5 EXERCISES
Exercise 1 · Warm-upPredict three next() calls

Follow the three results. What three things print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const letters = ["a", "b"];
const iterator = letters[Symbol.iterator]();
console.log(iterator.next().value);
console.log(iterator.next().done);
console.log(iterator.next().done);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeWrite a small range iterable

    Read or run the program. What does spreading the range print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function range(start, end) {
      return {
        [Symbol.iterator]() {
          let current = start;
          return {
            next() {
              if (current > end) return { done: true };
              return { value: current++, done: false };
            },
          };
        },
      };
    }
    console.log([...range(2, 4)].join(","));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGive pairs to a Map

      What prints after new Map reads the pairs?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const entries = [["theme", "dark"], ["font", "mono"]];
      const settings = new Map(entries);
      console.log(settings.get("font"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeFix a plain object loop

        Change the loop so it prints name=Ada and role=admin.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const user = { name: "Ada", role: "admin" };
        for (const item of user) {
          console.log(item); // TypeError: user is not iterable
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeSpot the early close

          This for...of loop stops with break after the first value. Predict the two lines it prints.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const iterable = {
            [Symbol.iterator]() {
              let value = 1;
              return {
                next() {
                  return value <= 3 ? { value: value++, done: false } : { done: true };
                },
                return() {
                  console.log("closed");
                  return { done: true };
                },
              };
            },
          };
          for (const value of iterable) {
            console.log(value);
            break;
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            Predict each answer first. Every answer says why it is right or wrong.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which method makes an object iterable?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const iterator = ["x"][Symbol.iterator]();
              console.log(iterator.next().value);
              console.log(iterator.next().done);

              Choose an answer to see the explanation.

            3. Question 3 of 8What does for...of do with the value on a { done: true } result?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this loop over a string print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              for (const character of "A😀B") {
                console.log(character);
              }

              Choose an answer to see the explanation.

            5. Question 5 of 8Which consumer calls return() when it stops early?

              Choose an answer to see the explanation.

            6. Question 6 of 8What does for...of get from a Map?

              Choose an answer to see the explanation.

            7. Question 7 of 8What happens when this loop runs on a plain object?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const plain = { a: 1 };
              for (const item of plain) {
                console.log(item);
              }

              Choose an answer to see the explanation.

            8. Question 8 of 8Which is true about the iterators that arrays, strings, Maps, and Sets make?

              Choose an answer to see the explanation.

            Key takeaways

            • An iterable has a [Symbol.iterator]() method. That method gives back an iterator.
            • An iterator has a next() method. Each call gives back a result, { value, done }.
            • for...of, spread, Array.from, destructuring, Promise.all, and new Map read values from iterables. They are consumers.
            • When a consumer stops early, JavaScript calls the iterator's return() method, if it has one.
            • Plain objects are not iterable. Use Object.entries, or give the object its own [Symbol.iterator]() method.

            Remember the one-liner.
            An iterable makes an iterator. Each next() call on the iterator gives back { value, done }.

            Up next: Generators, functions that make iterators for you with yield.

            CompleteFrontend Clear concepts. Working examples.