Search code examples
javascriptarrayspromisemethod-chaining

How Can I Unwrap an Array of Promises within a Chain of Array Methods?


I have a chain of several maps, one of which needs to perform a database operation for each array element, so I'm using async await.

const resultsAsPromises = arr
  .map(syncDouble)
  .map(asyncMapper)

This isn't a problem if it is the last item in the chain because I can unwrap it with Promise.all

console.log('results', await Promise.all(resultsAsPromises))

However, there are other synchronous operations I need to perform afterward, so I'd like to have the promises' values unwrapped before moving on to the next .map.

Is there a way to do this? I thought perhaps just making an extraction mapper like

function extractPromiseValues(value) {
  return value.then(v => v);
}

would work, but alas, no.

var arr = [1, 2, 3, 4, 5];
function timeout(i) {
  return new Promise((resolve) => {
    setTimeout(() => {
      return resolve(`number is ${i}`);
    }, 1);
  });
}

function syncDouble(i) {
  return i * 2;
}

async function asyncMapper(i) {
  return await timeout(i)
}

function extractPromiseValues(value) {
  return value.then(v => v);
}
async function main() {
  const resultsAsPromises = arr
    .map(syncDouble)
    .map(asyncMapper)
//     .map(extractPromiseValues)
  console.log('results', await Promise.all(resultsAsPromises))
}

main();

How can I unwrap an array of promises within a chain of array methods


Solution

  • Rather than passing an identity function to .then(), pass your synchronous operation instead, OR await the promise in an async function before passing it to your synchronous operation:

    function syncCapitalize(s) {
      return s.slice(0, 1).toUpperCase() + s.slice(1);
    }
    
    const resultsAsPromises = arr
      .map(syncDouble)
      .map(asyncMapper)
      .map(p => p.then(syncCapitalize)); // OR
    //.map(async p => syncCapitalize(await p));
    

    In the context of your example, this would look like:

    function timeout(i) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(`number is ${i}`);
        }, 1);
      });
    }
    
    function syncDouble(i) {
      return i * 2;
    }
    
    function asyncMapper(i) {
      return timeout(i);
    }
    
    function syncCapitalize(s) {
      return s.slice(0, 1).toUpperCase() + s.slice(1);
    }
    
    async function main() {
      const arr = [1, 2, 3, 4, 5];
      const resultsAsPromises = arr
        .map(syncDouble)
        .map(asyncMapper)
        .map(p => p.then(syncCapitalize)); // OR
      //.map(async p => syncCapitalize(await p));
    
      console.log('results', await Promise.all(resultsAsPromises));
    }
    
    main();

    Alternatively, if we are interpreting the question as Ghassen Louhaichi has, you could use the TC39 pipeline operator (|>) proposal to write the chain using one of the options below:

    F# Pipelines Proposal

    const results = arr
      .map(syncDouble)
      .map(asyncMapper)
      |> Promise.all
      |> await
      .map(syncCapitalize);
    

    Smart Pipelines Proposal

    const results = (arr
      .map(syncDouble)
      .map(asyncMapper)
      |> await Promise.all(#))
      .map(syncCapitalize);
    

    Unfortunately, unless you are using a Babel plugin, or until one of these proposals is merged into the official ECMAScript specification, you have to wrap the chain with await Promise.all(...):

    const results = (await Promise.all(arr
      .map(syncDouble)
      .map(asyncMapper)))
      .map(syncCapitalize);
    

    Finally, in the context of your full example:

    function timeout(i) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(`number is ${i}`);
        }, 1);
      });
    }
    
    function syncDouble(i) {
      return i * 2;
    }
    
    function asyncMapper(i) {
      return timeout(i);
    }
    
    function syncCapitalize(s) {
      return s.slice(0, 1).toUpperCase() + s.slice(1);
    }
    
    async function main() {
      const arr = [1, 2, 3, 4, 5];
      const results = (await Promise.all(arr
        .map(syncDouble)
        .map(asyncMapper)))
        .map(syncCapitalize);
    
      console.log('results', results);
    }
    
    main();