5

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

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
1252748
  • 14,597
  • 32
  • 109
  • 229
  • `.then(v => v)` is essentially a no-op (other than adding another microtick of delay to each promise resolution). – Patrick Roberts Jul 16 '20 at 16:44
  • I think the question is about the possibility of having a chain of `.map()` calls where, at some point, one returns an array of promises, and the next one returns an array of values that resolved from those promises. In that case, I'm not sure the answer below addresses that. Thoughts? – M0nst3R Jul 16 '20 at 17:10
  • Meaning, is `arr.map(returnValAfter1sec).map(doubleValAfterThat)` possible as written? (where `returnValAfter1sec` takes a value and returns a promise, and `doubleValAfterThat` takes a promise and returns a value). – M0nst3R Jul 16 '20 at 17:14
  • 1
    @GhassenLouhaichi I believe my edit has addressed your interpretation. Would you agree? – Patrick Roberts Jul 16 '20 at 17:40
  • I am thinking uses `Array.reduce` may be one solution because the forth parameter of its callback is the array itself. So inside the callback, we can use `Promise.all` for resolve all promises. – Sphinx Jul 16 '20 at 17:41
  • 1
    @Sphinx no you can't, because the return value of the `.reduce()` method would still be a promise. You'd have to insert an `await` preceding that section of the chain regardless of your approach. – Patrick Roberts Jul 16 '20 at 17:45
  • @PatrickRoberts many thanks, it does address it now, I was thinking and writing a solution in that direction and I realized that, like you said, there is no avoiding wrapping the asynchronous part of the chain with `(await )`. – M0nst3R Jul 16 '20 at 17:52
  • I'm guessing you'd really like a solution that allows you to stack up as many `.map(somethingSynchronous)` and `.map(somethingAsynchronous)` mappings as you choose, in any order. It's kind of possible but you need to make sure that (a) the functions are compatible with each other (that each returns a data type that the next function will accept) and (b) that the "synchronous" functions are written asynchronously (or promisified). – Roamer-1888 Jul 17 '20 at 19:15
  • @Roamer-1888 yeah that is definitely the idea. Throwing a `.filter` into the mix really failed quite miserably as well. – 1252748 Jul 17 '20 at 20:51

1 Answers1

4

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();
Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Exquisite! Can we vote on these proposals somewhere? – M0nst3R Jul 16 '20 at 17:53
  • And just to be pedantic, at the end of your last snippet, all you need is `console.log('results', resultsAsPromises);`, because the results are not promises anymore, in fact, the name of the array is misleading. – M0nst3R Jul 16 '20 at 17:56
  • 1
    @GhassenLouhaichi there's no place to "vote" per se, but a lot of active debate goes on in the [GitHub issues section](https://github.com/tc39/proposal-pipeline-operator/issues) to address use-cases, syntax, semantics, implementation details, etc. Generally speaking, the more attention one style gets, the more likely it will be chosen by the committee over the other. Just make sure you don't post superfluous issues for the sake of gaining attention though. – Patrick Roberts Jul 16 '20 at 17:58
  • 1
    @GhassenLouhaichi right thanks, I missed that. Copy-paste error. – Patrick Roberts Jul 16 '20 at 17:59
  • 1
    I think this is really good. `.map(p => p.then(syncCapitalize))` is just what I was looking for! I will have to dig in a little bit to understand this proposal. Wrapping in `Promise.all` is also not bad at all, maybe like on extra step to follow, but good nonetheless. Thanks very much Patrick. – 1252748 Jul 16 '20 at 18:01