JavaScript forEach vs for Loops

Over the past year of learning programming and software engineering, I came across different ways to perform loops. A loop is a powerful tool for repeating actions, but that’s not all to it. Today, I will be focusing on JavaScript’s Array.forEach() loop and the regular for loop.

Syntax Differences

The Array.forEach() loop is a higher-order array function, so it has to be used with arrays. On the other hand, for loops are more generic, and can be used for the sole purpose of wanting to repeat something. For meaningful comparison, we will take a look at the process of iterating through and logging the following array of fruit objects.

const fruits = [
  {name: "apple", weight: "100g"},
  {name: "banana", weight: "80g"},
  {name: "orange", weight: "70g"},
  {name: "grape", weight: "200g"}
]

// Basic for loop
for (let i = 0; i < fruits.length; i++) {
  console.log(`Name: ${fruits[i].name}, Weight: ${fruits[i].weight}`)
}

// Using forEach loop
fruits.forEach(fruit => console.log(`Name: ${fruit.name}, Weight: ${fruit.weight}`))

While both loops achieve the same outcome, the code is more readable using the forEach loop. In the for loop, an iterator i is used, and i also serves as the index of each fruit in the array. However, in the forEach loop, a callback function is passed in, which will execute for each fruit in the fruits array. As such, the amount of code written for a forEach loop is also reduced.

Obvious Differences

However, while there is a slight decrease in readability, for loops also offer better flexibility, since forEach loops execute the same callback for every object in the array. This flexibility is particularly useful for data processing or short-circuiting loops.

In the following example, we try to get all the fruits before hitting the first null object in the fruits array.

const fruits = [
  { name: "apple", weight: "100g" },
  { name: "banana", weight: "80g" },
  { name: "orange", weight: "70g" },
  null, null, null,
  { name: "grape", weight: "200g" }
]

// Basic for loop to add fruits before hitting null
let fruitBowl1 = []
let iterator1 = 0
const getFruit1 = () => {
  for (; iterator1 < fruits.length; iterator1++) {
    if (fruits[iterator1] === null) {
      return
    }
    fruitBowl1.push(fruits[iterator1])
  }
}
getFruit1()

// Using forEach loop to add fruits before hitting null
let fruitBowl2 = []
let iterator2 = 0
const getFruit2 = () => {
  fruits.forEach(fruit => {
    iterator2++
    if (fruit === null) {
      return
    }
    fruitBowl2.push(fruit)
  })
}
getFruit2()

console.log(fruitBowl1) // Only has first 3 fruits
console.log("for loop iterations: " + iterator1) // 3 iterations
console.log(fruitBowl2) // Has all fruits
console.log("forEach loop iterations: " + iterator2) // 7 iterations

It can be seen from this example that the return statement does not terminate the forEach loop, unlike the for loop. In fact, the return statement only exits the callback function supplied to the forEach loop, but does not change the fact that all fruits in the array are being processed.

For the record, the above code snippet is only an example to demonstrate behaviours, and is not representative of how people should use the forEach loop.

Subtle Differences

In reality, when working with backend systems, it is crucial to know when to use forEach and for loops. While seemingly serving the same purpose, when it comes to API calls for data fetching and processing, forEach loops are actually unable to await for Promises to be resolved.

Because of this, there can be unexpected outcomes when a forEach loop is used to resolve requests involving the awaiting of Promises. In some cases, it can be possible for the forEach to complete iteration before any data has been fetched or processed, and the remainder of the code below the loop runs.

This can cause issues to the integrity of the application, as other parts of the programme may rely on the fetched data.

This is made apparent using the example below.

Code for simulating API calls which return Promises

Since data fetching APIs are not instantaneous, I have included a 1s and 2s delay respectively for each of the “fetch APIs”.

const getFruitWeight = (index) => {
  return new Promise(resolve => {
    const fruits = [
      {name: "apple", weight: "100g"},
      {name: "banana", weight: "80g"},
      {name: "orange", weight: "70g"},
      {name: "grape", weight: "200g"}
    ]
    
    setTimeout(() => {
      resolve(fruits[index].weight)
    }, 1000)
  })
}

const getFruitName = (index) => {
  return new Promise(resolve => {
    const fruits = [
      {name: "apple", weight: "100g"},
      {name: "banana", weight: "80g"},
      {name: "orange", weight: "70g"},
      {name: "grape", weight: "200g"}
    ]
    
    setTimeout(() => {
      resolve(fruits[index].name)
    }, 2000)
  })
}

In this example, I will be attempting to make a bunch of API calls to a simulated backend, to request information about some fruits in the “database”, which contains an array of fruits and their names (N) and weights (W).

Each request will fetch either a fruit’s name (N) or weight (W) at a specific index.

// Required requests
const requests = [
  {index: 1, get: "W"}, {index: 2, get: "N"}, 
  {index: 3, get: "N"}, {index: 0, get: "W"}
]

The requests will be processed using either for or forEach loop, shown in the functions below.

const makeArrayUsingFor = async () => {
  let compiledArray = []
  for (let i = 0; i < requests.length; i++) {
    if (requests[i].get === "W") {
      compiledArray.push(await getFruitWeight(requests[i].index))
    } else {
      compiledArray.push(await getFruitName(requests[i].index))
    }
  }
  return compiledArray
}

const makeArrayUsingForEach = async () => {
  let compiledArray = []
  requests.forEach(async (request) => {
    if (request.get === "W") {
      compiledArray.push(await getFruitWeight(request.index))
    } else {
      compiledArray.push(await getFruitName(request.index))
    }
  })
  return compiledArray
}

In addition, I will also be using a performance logger to track how long each function call takes.

const { performance } = require('perf_hooks')

const printFetchedArrays = async () => {
  let startTime = performance.now()
  const forLoopArray = await makeArrayUsingFor()
  let endTime = performance.now()
  console.log(`For Loop: ${forLoopArray} - time: ${endTime - startTime}ms`)
  startTime = performance.now()
  const forEachLoopArray = await makeArrayUsingForEach()
  endTime = performance.now()
  console.log(`For Each Loop: ${forEachLoopArray} - time: ${endTime - startTime}ms`)
}

printFetchedArrays()

After running the above code, the delay caused by the setTimeouts used can be felt during the execution of the for loop, and the results of the data fetching and processing also show that the requests were being fetched and processed in the correct order.

However, the delay in the execution of the forEach loop is unnoticeable. It is also apparent that the data was not fetched and processed at all, meaning that the programme did not wait for the Promises to be resolved.

The results are as shown, when executed using node.

$ node forEach-vs-for.js 
For Loop: 80g,orange,grape,100g - time: 6038.477099999785ms
For Each Loop:  - time: 0.2986999973654747ms

Possible Remedies

Since it was mentioned that forEach loops are unable to return any values or Promises, there is nothing to await, and thus, the above implementation for fetching data failed.

This does not mean that higher-order array functions are unable to perform data fetching from APIs. If the higher-order array function was able to return Promises to be resolved, then it can be used with awaits, which will allow it to fetched and processed accordingly.

In fact, if the order of fetching was irrelevant, a more efficient approach to data fetching would be to use an Array.map() function to fire all the requests together, and return an array of Promises as a result. The Promise.all() method can be used with the array of Promises to resolve them all at once, achieving seemingly parallel execution.

The function to perform the Array.map() function is shown below.

const makeArrayUsingMap = async () => {
  let compiledArray = await Promise.all(requests.map(async (request) => {
    if (request.get === "W") {
      return await getFruitWeight(request.index)
    } else {
      return await getFruitName(request.index)
    }
  }))
  return compiledArray
}

By using this implementation instead, the execution time is lowered as the longest fetch took 2 seconds, and since they were resolved in parallel, the overall execution took roughly 2 seconds as well.

$ node forEach-vs-for.js 
For Loop: 80g,orange,grape,100g - time: 6025.564899999648ms
Map: 80g,orange,grape,100g - time: 2012.9765999987721ms

In this example, we managed to preserve the order of requests, but it must be noted that this is NOT guaranteed.

Conclusion

It has been 5 months since I last racked my brain this hard outside of school work. This article was actually inspired by the development of the application I am currently working on.

Hopefully this helps anyone who is a JS Developer.

Full code on GitHub: forEach-vs-for.js

Return to Posts