Slice of Dev Logo

Promises with Loops and Array Methods in JavaScript

Cover Image for Promises with Loops and Array Methods in JavaScript
Author's Profile Pic
Rajkumar Gaur

Promises are objects that represent the eventual completion (or failure) of an asynchronous operation and allow you to handle the result of that operation when it completes. Loops are used to execute a block of code multiple times.

However, when working with asynchronous code, it’s important to understand how loops behave and how to use promises to control the flow of execution.

Usage with for..of loop

One common use case for using promises with loops is when you need to perform an operation multiple times, but each operation depends on the result of the previous one. In this case, you can use a loop to iterate over the data and a promise to control the flow of execution.

Let’s take a look at an example:

const items = [1, 2, 3, 4, 5];

function processItem(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Processing item ${item}`);
      resolve(item);
    }, Math.random() * 1000);
  });
}

async function processItems() {
  let result = 0
  for (const item of items) {
    result = await processItem(result + item);
  }
  console.log('All items processed');
}

processItems();

Executing this code will work sequentially as expected adding the result of the previous item to the current item. See the following output:

Processing item 1
Processing item 3
Processing item 6
Processing item 10
Processing item 15
All items processed

But, if waiting for the previous result is not required or the order of execution doesn’t matter, it is best not to wait for the results and remove the keywords async and await .

This way, the code can execute concurrently without blocking the main thread. See the following code and output:

function processItems() {
  for (const item of items) {
    processItem(item);
  }
}

processItems();
console.log('All items sent for processing');

All items sent for processing
Processing item 4
Processing item 2
Processing item 5
Processing item 1
Processing item 3

Sometimes it is required to wait for all the concurrent tasks before returning from the function or exiting the code. Promise.all can be used in this case to wait for the result.

async function processItems() {
  const promiseArray = []
  for (const item of items) {
    promiseArray.push(processItem(item));
  }
  const result = await Promise.all(promiseArray);
  console.log(result);
}

processItems();
console.log('All items sent for processing');

All items sent for processing
Processing item 2
Processing item 4
Processing item 5
Processing item 3
Processing item 1
[ 1, 2, 3, 4, 5 ]

Usage of Promises with traditional for(let i=0;i<10;i++) loops will also behave like the above examples.

Usage with Array Methods

Often there are times when an asynchronous operation needs to be carried out for each item of an array. Let’s take a look at the different ways of doing that.

Using forEach

We can use the forEach() method to iterate over the array, and use Promises to execute the asynchronous operation for each object concurrently. Here's how it might look:

async function processItems() {
  items.forEach((item) => {
    processItem(item);
  })
}

processItems();
console.log('All items sent for processing');

All items sent for processing
Processing item 2
Processing item 5
Processing item 1
Processing item 3
Processing item 4

As forEach takes a function as an argument, each item is processed separately in the function. So even if the async and await keywords are added to the function, forEach will still run the asynchronous code concurrently.

// still runs concurrently

async function processItems() {
  items.forEach(async (item) => {
    await processItem(item);
  })
}

It’s recommended to use for..of loop if it is required to wait for each promise to complete or to run the asynchronous operations sequentially for each item.

The promises can be executed sequentially by chaining the promises but it's generally not a good practice. See the following example for the promise chain trick.

function processItems() {
  let chainedPromise = Promise.resolve();
  items.forEach((item) => {
    chainedPromise = chainedPromise.then(() => {
        return processItem(item);
    })
  });
}

processItems();
console.log('All items sent for processing');

Using map

map method behaves very similarly to forEach with the difference being it allows to return a value for each item of an array. Let’s rewrite the above examples with map .

function processItems() {
  items.map((item) => {
    processItem(item);
  })
}

processItems();
console.log('All items sent for processing');

The above example runs concurrently to produce the following output.

All items sent for processing
Processing item 5
Processing item 2
Processing item 3
Processing item 4
Processing item 1

map makes it easier for using Promise.all as it allows us to return a value so that we can wait for the results of the promises.

Let’s return the promise from the map function and use it in a Promise.all

async function processItems() {
  const promiseArray = items.map((item) => {
    return processItem(item);
  })
  const result = await Promise.all(promiseArray);
  console.log(result);
}

processItems();
console.log('All items sent for processing');

All items sent for processing
Processing item 1
Processing item 3
Processing item 5
Processing item 4
Processing item 2
[ 1, 2, 3, 4, 5 ]

The code still runs concurrently and then awaited for all the Promises to settle. The promise chain trick we saw previously can be used with map if the Promises need to be executed sequentially.

async function processItems() {
  let chainedPromise = Promise.resolve();
  const promiseArray = items.map((item) => {
    return chainedPromise = chainedPromise.then(() => { 
      return processItem(item)
    });
  })
  const result = await Promise.all(promiseArray);
  console.log(result);
}

processItems();
console.log('All items sent for processing');

All items sent for processing
Processing item 1
Processing item 2
Processing item 3
Processing item 4
Processing item 5
[ 1, 2, 3, 4, 5 ]

Error Handling

When working with Promises, there’s always the possibility of an error occurring. It’s important to handle these errors to prevent the program from crashing or behaving unexpectedly.

The following error is generally thrown when a Promise rejection error is not handled properly.

UnhandledPromiseRejectionWarning: Unhandled promise rejection ...

Handling with the .catch method

The errors should be handled with the .catch method on a Promise object. Otherwise, the process will be terminated with a non-zero exit code.

The examples above for .forEach and .map can be modified to handle the errors with the .catch method.

function processItem(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Processing item ${item}`);
      Math.random() > 0.5 
        ? resolve(item) 
        : reject(`error occurred for ${item}`);
    }, Math.random() * 1000);
  });
}

function processItems() {
  items.forEach((item) => {
    processItem(item).catch(err => {
      console.log(err);
    })
  })
}

This will gracefully handle the errors and log them to the console. Otherwise, it would have crashed the program.

All items sent for processing
Processing item 5
Processing item 3
Processing item 4
error occurred for 4
Processing item 2
error occurred for 2
Processing item 1
error occurred for 1

Handling errors in Promise.all

If any promise is rejected from the promise array passed to Promise.all , Promise.all will also reject and throw an error. To fix this, again, every promise should end with a .catch method.

async function processItems() {
  const promiseArray = items.map((item) => {
    return processItem(item)
             .catch(err => console.log(err));
  })
  const result = await Promise.all(promiseArray);
  console.log(result);
}

All items sent for processing
Processing item 2
Processing item 3
Processing item 1
error occurred for 1
Processing item 5
Processing item 4
error occurred for 4
[ undefined, 2, 3, undefined, 5 ]

Promise.allSettled can also be used as an alternative, it will always resolve even if any or all of the promises throw an error.

async function processItems() {
  const promiseArray = items.map((item) => {
    return processItem(item);
  })
  const result = await Promise.allSettled(promiseArray);
  console.log(result);
}

All items sent for processing
Processing item 3
Processing item 5
Processing item 2
Processing item 4
Processing item 1
[
  { status: 'rejected', reason: 'error occurred for 1' },
  { status: 'fulfilled', value: 2 },
  { status: 'fulfilled', value: 3 },
  { status: 'fulfilled', value: 4 },
  { status: 'rejected', reason: 'error occurred for 5' }
]

Handling errors using try/catch block

An alternative to .catch method is using traditional try {...} catch() {...} blocks. But this should only be used if the promise is awaited using the await keyword. try/catch will have no effect if the promise is not awaited.

function processItems() {
  items.forEach(async (item) => {
    try {
      await processItem(item);   
    } catch(err) {
      console.log(err);
    }
  })
}

Note that the following code would still throw an error because try/catch block only handles promises awaited with the await keyword.

function processItems() {
  items.forEach((item) => {
    try {
      processItem(item);   // error
    } catch(err) {
      console.log(err);
    }
  })
}

Conclusion

Working with Promises inside loops and array methods can be tricky for beginners. I hope this article helped you in grasping this concept.

Thank you for reading and see you at the next one!


Cover Image for React Interview Experience

React Interview Experience

This blog is about my recent React interview experiences and some interesting questions that were asked. These questions might help you prepare for your next interview. Guess The Output | useState vs useReducer | useCallback and useMemo | Redux Vs Context API | Manage The Focus Of Elements Using React | Why is useRef used | Coding Problem.

Author's Profile Pic
Rajkumar Gaur
Cover Image for Streams in NodeJS

Streams in NodeJS

Node.js Streams are an essential feature of the platform that provide an efficient way to handle data flows. They allow for processing of large volumes of data in a memory-efficient and scalable way, and can be used for a variety of purposes such as reading from and writing to files, transforming data, and combining multiple streams into a single pipeline

Author's Profile Pic
Rajkumar Gaur