Three Reasons Why JavaScript forEach is Special

Three Reasons Why JavaScript forEach is Special

Subtle differences that make the JavaScript forEach method different from the rest.

·

4 min read

The other day, I found an interesting code snippet in the 'Spot The Bug' section of a Bytes newsletter. Can you spot it?

const getCapitalizedInitials = (name) =>
  name
    .trim()
    .split(" ")
    .forEach((name) => name.charAt(0))
    .join("")
    .toUpperCase()

Since this article is about the forEach method, there's no surprise that the bug has something to do with the usage of the method. But what is it? Keep reading on to find out!

Here are three things that make the forEach method in JavaScript special compared to the other array methods and loops that the language offers.

Return Value

Out of all the iteration methods available on Arrays, forEach is the only one that doesn't have a return value. It iterates over each element and executes a callback function for each iteration, but the method itself returns undefined at the end of the iteration.

This is unlike the other methods where a single value (find, findIndex, some) or an array itself (map, filter,slice) is returned. Because the return value is undefined, forEach cannot be chained with other array methods. That is the bug in the code snippet above. In our case, forEach should be replaced with map.

Jump Statements

Since there is no return value, it's natural to approach forEach as an alternative to loops (for, while, for-of, for-in etc). I like to think of the method as a for-loop on steroids, but there is a difference here too. Jump statements (break and continue) have no effect inside the forEach callback. This is because, unlike regular loops, forEach doesn't have a loop body. It has a callback function instead that is called for each iteration. Callback functions are just like regular functions and don't have jump statements.

There are a few ways to mimic those behaviors if they are absolutely necessary. A return statement can be used inside the callback to mimic the continue statement. However, there isn't an easy way to stop or break the iteration. The only way is by throwing an exception. This is not a recommended practice. If early termination is necessary, other looping statements, like for-of or for-in can be used.

const numbers = [1, 2, 3, 4 , 5, 6, 7, 8, 9, 10];


// regular loop that skips iteration for all even numbers
// and breaks it when 7 is reached
for(let i = 0; i < 9; ++i) {
    if(i % 2 === 0)
        continue;

    if(i == 7)
        break;

    console.log(i);
}
/* output: 
1
3
5
*/


// forEach method that tried to mimic the same behavior
numbers.forEach(num => {
    if(num % 2 === 0)
        continue; // error

    if(num === 7)
        return; // mimics the break statement

    console.log(num)
})

Promises

forEach is not designed to work with Promises and asynchronous code in general. For the code snippet below, we might expect the await statement to work as it normally does, but it doesn't. forEach does not wait for each Promise to resolve, so all of them are executed in parallel.

Because of this, the iteration finishes before all the Promises are resolved, and the code execution continues. This is why 'end forEach' is logged before the console.log statements inside the callback function.

const numbers = [1, 2, 3, 4, 5, 6];

const wait = function(time) {
  return new Promise(resolve => setTimeout(resolve, time * 1000))
};

(async function(){
  console.log('start forEach');

  await numbers.forEach(async(num) => {
    await wait(num);
    console.log(num)
  });

  console.log('end forEach')
})();

/* output:
start forEach
end forEach
1
2
3
4
5
6
*/

If you need to work with async code, it is recommended to use for-of or the regular for loop that supports asynchronous JavaScript.

 (async function(){
  console.log('start for-of');

  for(const num of numbers) {
    await wait(num);
    console.log(num)
  }

  console.log('end for-of')
})();

/*
output: 
start for-of
1
2
3
4
5
6
end for-of
*/

Recap

  • forEach doesn't have a return value, and hence cannot be chained with other array methods.

  • Jump statements, like continue and break do not work inside the callback function.

  • forEach is not designed to work with asynchronous JavaScript.

Further Reading

  1. A three-part series on fundamental array methods - map, filter, reduce

  2. A great read about not using forEach with async-await

  3. Handling Promises in parallel

  4. Three for loops in JavaScript

  5. forEach MDN

Did you find this article valuable?

Support Abin John by becoming a sponsor. Any amount is appreciated!