Use async / await with Javascript's .map() and other high-order functions

High order functions in Javascript are incredibly useful to improve code readability and to reduce redundancies. They have a weak point, however: Asynchrony. Read on to learn how to tackle this matter.

TL: DR - Asynchronous, high order array functions return an array of promises. To resolve each of these, you can use one of the following methods:

  • Promise.all([ /* ... array of promises ... */ ])
    Wait for all promises to be resolved. Throws errors
  • Promise.allSettled([/* ... array or promises ...*/ ])
    Wait for all promises to be resolved or rejected. Requires manual error handling

A variable assignation using .map() then looks something like this:

const promiseArray = iterableArray.map(async (element) => {
  // ... async code 
  return result;  
});

const dataArray = Promise.all(promiseArray);

A simple use case

While high-order functions have lots of perks, I recently noticed they were not natively capable of handling promises' syntactic sugar very well.

I noticed this problem when developing on serverside Node.js code, which was meant to accept an array of files from an incoming client as form data and save it to a database. Instead of returning the response, I'd expect, namely an array with values, the below function returned me an array of Promises:

  • First, the npm Formidable library would handle form parsing and give me a files - object. It would be available only inside the callback's scope.
  • Inside files, the first property would indicate the file - array:
    const filePropertyName = Object.keys(files)[0]
  • Having identified these two, I could now iterate through the array of files.
  • For each file, I would then prepare a payload and call an SQL - stored procedure to asynchronously write this file into the database, using mssql.
  • Each successfully performed stored procedure would return me a field that uniquely identifies each uploaded file. I would store it in fileIds (see code below) and then send the array back to the client.

So far so good, right? Everything that comes after cannot be much harder. Here's the code:

// Iterate through the array of files identified by its form property
// ('name' of the client's form field)
const fileIds = files[filePropertyName].map(async (file /* object */) => {

  // Use a private function to create a payload for stored procedure
  // (In order to work, it required some intel from other formfields)
  const payload = this._handleSetUploadPayload(fields,file);
  
  // Create a new FileModel 
  const File = new FileModel(storedProcedureName);
  
  // Use its uploadFile method to trigger the stored procedure
  return await File.uploadFile(payload);
});

Well, not so fast. After sending three files down the API, what was contained in fileIds was not exactly what I've been looking for. When I started to debug, I saw the following result:

[Promise {<pending>}, Promise {<pending>}, Promise{<pending>}]

I was puzzled for a moment. And frustrated. So I started searching MDN and found an explanation (step 9: return A).

The solution

In my own words, that'll be:

The .map() algorithm applies an async callback to each element of an array, creating promises as it does. However, the returned result  by .map() is no promise, but an array of promises.

That was an answer I could live with. So I changed the code accordingly, primarily by adding Promise.all() and - voila, it started to work:

const fileIdsPromises = files[filePropertyName].map(async (file) => {
  const payload = this._handleSetUploadPayload(fields,file);
  const File = new FileModel(storedProcedureName);

  const fileId = await File.uploadFile(payload);
  return fileId
});

const fileIds = await Promise.all(fileIdsPromises);