Stop Using forEach!

The deepest circles of JavaScript hell are reserved for people that use forEach.

krakenTentacles.forEach(tentacle => tentacle.mutate()); // uh-oh!

To understand why this cursed method exists, we need to look at the original ways to loop over an array.

The safe option was the for loop.

for (var i = 0; i < krakenTentacles.length; i++) {
  krakenTentacles[i].mutate();
}

And the other was the for..in loop.

for (var i in krakenTentacles) {
  krakenTentacles[i].mutate();
}

It may look friendlier but for..in has some big problems.

At the time, var was the only way to define a local variable. This meant that both loop's variables would be visible outside of the loop block, which caused all kinds of bugs.

And lo' and behold, the fifth edition of ECMAScript appeared with a method for safely iterating over arrays: forEach!

krakenTentacles.forEach(function(tentacle, i) {
  console.log(i, number);
});

This seems like a reasonable way to solve some legitimate problems without changing the grammar of the language.

However... JavaScript has improved a lot since forEach first appeared. Most of these problems have better solutions, and the language has grown in ways that forEach doesn't understand.

forEach doesn't cut it any more, and you shouldn't be using it in new code!

It's not a loop!

You can't return a value early.

function findLoadedCannon(cannons) {
  cannons.forEach(cannon => {
    if (cannon.isLoaded()) {
      return cannon; // Has no effect on the outer function.
    }
  });
}

You can't break or continue.

escapeRoutes.forEach(escapeRoute => {
  if (escapeRoute.isOpen()) {
    break; // SyntaxError
  }
});

You can't use await or yield.

async function prayForDeliverance(gods) {
  gods.forEach(god => {
    let answer = await prayTo(god); // SyntaxError
    callOut(answer);
  });
}

forEach regularly causes confusion in asynchronous code, where it looks like you can solve the problem by making the callback function async too.

async function prayForDeliverance(gods) {
  let answers = [];
  await gods.forEach(async god => {
    let answer = await prayTo(god);
    answers.push(answer);
  });
  return answers;
}

forEach quietly ignores the async nature of the callback, and await quietly ignores the undefined that forEach returns.

prayForDeliverance will return a promise that always resolves to an empty array.

Then at some point in the future, the fetch calls start to resolve in an unpredictable order, and that array starts to mutate underneath you.

You're dealing with race conditions, asynchronous mutations, and unhandled promise exceptions in this little function, all because you used forEach.

It Only Works With Arrays

You can't use forEach to iterate over a string.

"shipwreck".forEach();
// undefined is not a function

Or a Map or a Set.

new Map([[3, "buckets"], [2, "ropes"]]).forEach();
// undefined is not a function

new Set(["spyglass", "compass", "rum"]).forEach();
// undefined is not a function

Or any other third party data structure that implements the iterable protocol.

import { PegLegTree } from "@pirates/trees";

new PegLegTree(23, 54, 64, 12).forEach();
// Only works if `PegLegTree` has implemented a `forEach` method.
forEach actually tried to solve this problem before iterables existed!

When forEach appeared, arrays were not the only thing people needed iterate over. The language designers knew this, and they included the following note in the specification:

The forEach function is intentionally generic; it does not require that its this value be an Array object.

Therefore it can be transferred to other kinds of objects for use as a method. Whether the forEach function can be applied successfully to a host object is implementation-dependent.

Here's an example of "transferring" forEach to a string.

Array.prototype.forEach.call("shipwreck", char => {
  console.log(char);
});

It works (in an implementation-dependent sense), but it's not pleasant. It's not obvious why the prototypal inheritance model is leaking out into the code.

Behind the scenes it is checking for a numeric length property, then attempting to iterate over the indexes.

This means that you get some interesting behaviours when you call forEach on an object that's pretending to be an array.

// Don't run this unless you want to crash your browser
Array.prototype.forEach.call({ length: Infinity }, console.log);

Save yourselves!

forEach is a sinking ship that we need to abandon. Thankfully, for..of is our lifeboat!

for (let tentacle of krakenTentacles) {
  tentacle.mutate();
}

You can return from inside.

function findLoadedCannon(cannons) {
  for (let cannon of cannons) {
    if (cannon.isLoaded()) {
      return cannon;
    }
  });
}

You can break and continue.

for (let escapeRoute of escapeRoutes) {
  if (escapeRoute.isOpen()) {
    break;
  }
});

You can use await and yield.

async function prayForDeliverance(gods) {
  for (let god of gods) {
    let answer = await prayTo(god);
    callOut(answer);
  }
}

The for..of loop works with any object that implements the Iterable protocol (including Array, String, Map and Set).

It also works with let and const to create block scoped variables that are only visible inside the loop.

You can fall back to a for loop if you need the index variable too.

forEach was the short-term fix, but for..of is the long-term solution!