How to Sequentially Resolve an Array of Promises in JavaScript

If you use JavaScript, either with plain-old JS in the browser, React, or Node.js, you are probably familiar with Promises. Furthermore, you might have also come across resolving arrays of Promises with the Promise.all() function.

Here is an example of Promise.all:

const getSquare = async (x) => Math.pow(x, 2);
const printSquares = async () => {
     const nums = [1, 2, 3, 4, 5];
     const promiseArray = nums.map(x => getSquare(x));

     const resolvedPromises = await Promise.all(promiseArray);
     console.log(resolvedPromises);
};

printSquares();

getSquare returns a Promise and as the name states promiseArray contains an array of Promises. All of these Promises need to be resolved before getting the square. If you don’t this, what will be printed to the console is as follows:

[
  Promise {
    1,
    [Symbol(async_id_symbol)]: 36,
    [Symbol(trigger_async_id_symbol)]: 5,
    [Symbol(destroyed)]: { destroyed: false }
  },
  Promise {
    4,
    [Symbol(async_id_symbol)]: 37,
    [Symbol(trigger_async_id_symbol)]: 5,
    [Symbol(destroyed)]: { destroyed: false }
  },
  Promise {
    9,
    [Symbol(async_id_symbol)]: 38,
    [Symbol(trigger_async_id_symbol)]: 5,
    [Symbol(destroyed)]: { destroyed: false }
  },
  Promise {
    16,
    [Symbol(async_id_symbol)]: 39,
    [Symbol(trigger_async_id_symbol)]: 5,
    [Symbol(destroyed)]: { destroyed: false }
  },
  Promise {
    25,
    [Symbol(async_id_symbol)]: 40,
    [Symbol(trigger_async_id_symbol)]: 5,
    [Symbol(destroyed)]: { destroyed: false }
  }
]

instead of the desired output:

[ 1, 4, 9, 16, 25 ]

Sequentially Resolving Promises

In the above example the order of the getSquare being resolved doesn’t matter…note that the returned array’s element will always correspond to the array of Promises.

However, say order does matter or doing parallel processing creates system issues. For example, I recently ran into an issue calling the Firebase getUser function in parallel a few hundred times. At times a Firestore error occurred because the database was overloaded.

const uids = ["id1", "id2",...];
const userPromises = uids.map(uid => admin
   .auth()
   .getUser(uid)
   .then((userRecord) => {
      return userRecord.toJSON();
   })
   .catch(console.error)
);

const users = await Promise.all(userPromises); // The problem!

Luckily the processing is not time sensitive or intensive, so doing the processing in sequence is acceptable.

Side Note: The JavaScript runtime doesn’t actually process in parallel and is not multi-threaded, but rather has an event loop to perform non-blocking I/O operations. For example, if a Promise calls an external URL, the runtime will process then next task on the event loop as it waits for the response from the called URL. This makes the processing seem multi-threaded and fast. For this reason, processing sequentially is usually much slower since you’re blocking until the Promise fully completes.

At first, you might think to use the forEach … Don’t ! The forEach doesn’t play nicely with aysnc and will cause problems.

However, the ES 2018 version of for plays quite nicely for sequential processing with await*:

const resolvePromisesSeq = async (tasks) => {
  const results = [];
  for (const task of tasks) {
    results.push(await task);
  }

  return results;
};

The resolvePromisesSeq will take an array of tasks and then process each one in order.

Let’s update the Firebase code to be sequential:

const uids = ["id1", "id2",...];
const userPromises = uids.map(uid => admin
   .auth()
   .getUser(uid)
   .then((userRecord) => {
      return userRecord.toJSON();
   })
   .catch(console.error)
);

const users = await resolvePromisesSeq(userPromises); // No longer a problem!

Beautiful!

And the new function is reusable whenever you need to process Promises sequentially.

*Thanks to getify for recommending using the await within the for instead of at the top level.