JavaScript for…of Loop and Iterables Explained

The JavaScript for...of statement creates a loop iterating over iterable objects, including built-in types like Array, String, Map, Set, and custom data structures. This article explores the mechanics of how for...of interacts with the JavaScript Iteration Protocols, detailing the role of Symbol.iterator, the execution of the .next() method, and how the loop handles completion and early termination.


The Iteration Protocol

To understand how for...of works, you must understand the two protocols that govern iteration in JavaScript: the Iterable Protocol and the Iterator Protocol.

  1. Iterable Protocol: An object is iterable if it defines a method at the key [Symbol.iterator]. This method is a factory function that returns an iterator object.
  2. Iterator Protocol: An iterator is an object that implements a next() method. Calling next() returns an object with two properties:
    • value: The current item in the sequence.
    • done: A boolean indicating whether the sequence has finished (true) or has more values (false).

How for...of Executes Under the Hood

When a for...of loop executes on a target object, it performs the following steps automatically:

  1. Retrieves the Iterator: It checks for the [Symbol.iterator] method on the target object. If the method does not exist, the engine throws a TypeError: [object] is not iterable.
  2. Calls the Iterator Method: It invokes target[Symbol.iterator]() to obtain a fresh iterator instance.
  3. Calls .next(): On each iteration cycle, the loop calls iterator.next().
  4. Evaluates done:
    • If done is false, the loop assigns value to the loop variable and executes the code block.
    • If done is true, the loop terminates immediately without executing the block for that step.

Equivalent Manual Implementation

The behavior of for...of can be represented manually using a while loop:

const collection = ['a', 'b', 'c'];

// How for...of works internally:
const iterator = collection[Symbol.iterator]();
let result = iterator.next();

while (!result.done) {
  const item = result.value;
  console.log(item); // Process the item
  result = iterator.next();
}

Working with Built-in Iterables

JavaScript provides built-in [Symbol.iterator] implementations for several core objects:

Note: Plain JavaScript objects ({}) are not iterable by default because they do not implement [Symbol.iterator].


Creating Custom Iterables

You can make any custom object compatible with for...of by implementing the [Symbol.iterator] method.

const range = {
  from: 1,
  to: 3,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;

    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (const num of range) {
  console.log(num); // Logs: 1, then 2, then 3
}

Early Termination and Cleanup with return()

If a for...of loop exits early—via a break, return, or an uncaught exception—the loop checks if the iterator object has a return() method.

If return() is present, for...of automatically invokes it before exiting. This allows iterators to perform cleanup operations, such as closing file handles, freeing memory, or closing network streams.

const cleanableIterable = {
  [Symbol.iterator]() {
    return {
      next() {
        return { value: 'data', done: false };
      },
      return() {
        console.log('Cleanup logic executed.');
        return { done: true };
      }
    };
  }
};

for (const item of cleanableIterable) {
  console.log(item);
  break; // Triggers iterator.return()
}
// Output:
// 'data'
// 'Cleanup logic executed.'

Key Differences: for...of vs. for...in