3

I know that we can't simply do something like:

myArray.forEach(async x => await asyncOperation())

Edit: I know this is valid, but I need things to be in the proper order

and if we do have to iterate through an array with an asynchronous operation, I have to instead do:

await Promise.all(myArray.map(x => asyncOperation()))

However, I need to do two async operations within the same iteration. I know another alternative is to just use .reduce which will look something like this:

await myArray.reduce((p, el) => {
      return p.then(() => {
        return somePromise(el)
          .then(res => {
            return anotherPromise(res)
          })
      })
    }, Promise.resolve())

But I am avoiding nesting promises and would like to keep it just with async/await. So anyway, my question is, what is the best wait to iterate over an array that needs to go through two promises?

theJuls
  • 6,788
  • 14
  • 73
  • 160
  • "I know that we can't simply do..." why can't you? that code is perfectly valid – I wrestled a bear once. Sep 10 '19 at 15:45
  • If I remember correctly, it doesn't fire the promises off in sequence, which I do need. This thread covers it: https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop – theJuls Sep 10 '19 at 15:48
  • Aaaand that code seems to also answer my question as to an alternative way of doing this – theJuls Sep 10 '19 at 15:48

3 Answers3

0

async/await are great, but they're not a 100% replacement for all promise-using code. Sometimes (eg. when you need to use Promise.all) you do need to use the promises as values themselves, and not used secretly inside async/await.

There are ways to avoid doing that, like eschewing the ES6 map and other array iteration methods, and instead using for loops ... but usually doing that is ... well the cure is worse than the illness. Just use promises.

EDIT:

Here's a basic example based on comment feedback. Let's say you have two (or more) URLs, and want to fetch the first one THEN (after it returns), fetch the next one, and once they're ALL fetched do something else. If you try to do that synchronously with ES6 array methods, even if you use async/await, it won't work:

const urls = ['www.example.com/1', 'www.example.com/2'].
var promises = urls.map(async url => await fetch(url));
Promise.all(promises).then(doSomethingElse);

What that code will actually do is immediately fetch all the URLs; it won't wait to fetch the second until the first is done.

To do that you could use async/await with a for loop and avoid the ES6 stuff (presumably the following is inside an async function):

const urls = ['www.example.com/1', 'www.example.com/2'].
const pageHtml = [];
for (var i = 0; i < promises.length; i++) {
  const url = urls[i];
  const html = await fetch(url));
  pageHtml.push(html);
}
doSomethingElse(); // no need for Promise.all!

... but then you're using for loops like it's 1999; ick! The better solution in my opinion is to use modern Javascript, but just don't use async/await for everything.

Use those keywords most of the time, but then when you need to use Promise.all either use a recursive function (if you need the "fetch #1 AFTER #2" part; see @jcubic's answer for a possible implementation), or if you don't just do:

const urls = ['www.example.com/1', 'www.example.com/2'].
var promises = urls.map(fetch);
Promise.all(promises).then(doSomethingElse);

Yes you are avoiding async/await and using an array or Promise objects directly with Promise.all ... but the code is short and clear and does what you want. You don't have to always use those keywords.

machineghost
  • 33,529
  • 30
  • 159
  • 234
  • Async/Await are just syntactic sugar. All they do is wrap what you return like so: `async () => await 1` becomes `() => new Promise(resolve => resolve(1))`. You can access that promise when it's returned just like any other, and you can then combine it with others using `Promise.all` ... but you can't overcome the natural rules of Javascript and (for instance) iterate through promises using an ES6 `forEach` (or rather you can, but they'll be no waiting; all the callbacks will happen at once, not sequentially after the previous one finishes). None of that invalidates my answer. – machineghost Sep 10 '19 at 16:01
  • i'm still not clear. you say i'm mistaken and then you show me a function that returns a promise, which is exactly what i said. async functions always return a promise. how am i mistaken? – I wrestled a bear once. Sep 10 '19 at 16:03
  • You're mistaken if you think you can use `async`/`await` with ES6 array iteration methods. And if you're not arguing with that, I really don't know what you're arguing with. This is basic Javascript stuff, there's nothing controversial in my answer. – machineghost Sep 10 '19 at 16:04
  • a promise is just another type of object. of course you can use an array of promises in the same way you use an array containing any other type of data. – I wrestled a bear once. Sep 10 '19 at 16:05
  • I feel like you're not understanding of asynchronicity. When you *synchronously* iterate through an array you handle member #1, then #2, etc. *Asynchronous* code is NOT like that. If you want to say "fetch site #1 THEN fetch site #2" you can't use a *synchronous* approach like ES6 iteration methods. They won't "fetch site #1, THEN fetch site #2": they will fetch site #1 AND site #2 AND all other sites, at once. This isn't even async/await, this is just promises. The only thing asyc/await does is hide promise details ... but again, if you want to use `Promise.all`, you need the promise! – machineghost Sep 10 '19 at 16:08
  • See edited answer. – machineghost Sep 10 '19 at 16:26
-1

Shameless plug incoming, I suggest the npm package bs-better-stream, which specializes in async streams / arrays.

For your example, it could be as simple as:

const stream = require('bs-better-stream');

let s = stream()
  .write(...myArray)
  .map(el => somePromsie(el))
  .wait() // or waitOrdered() to preserve original order
  .map(res => anotherPromise(res))
  .wait();

It is slightly more verbose if you want to convert back into a vanilla array because you'll need to wait for both sets of promises to resolve. But it's still significantly more readable than .reduce or (await Promise.all(...)).map:

const stream = require('bs-better-stream');

let somePromise = a => Promise.resolve(a * 2);
let anotherPromise = a => Promise.resolve(-a);

let x = async myArray => {
    let promises = stream()
        .write(...myArray)
        .map(el => somePromise(el));

    let promises2 = promises
        .wait()
        .map(res => anotherPromise(res));

    await promises.promise;
    return promises2.promise; // -2, -4, -6, -8
}

x([1, 2, 3, 4]).then(a => console.log(a));

Another advantage to this approach is that it allows all async functions to happen in parallel; i.e. the 2nd set of promises don't have to wait for all of the first set of promises to complete.

junvar
  • 11,151
  • 2
  • 30
  • 46
-1

You can use async loop with recursive function, I usually do code this way, even if I can't use async/await

return (async function next() {
    var item = arr.shift();
    if (shift) {
      var res = await doSomething(item);
      await doSmethingElse(res);
      next();
    }
})();

this should be a pattern, I do this all the time but usually it look like this (I still write some ES5 code by hand).

return new Promise(resolve) {
   (function next() {
       var item = arr.shift();
       if (item) {
          doSomething(item).then(doSmethingElse).then(next);
       } else {
          resolve();
       }
   })();
});

and if you want to accumulate the results of doSmethingElse just add them to array and pass to resolve same as with async/await example.

return new Promise(resolve) {
   var result = [];
   (function next() {
       var item = arr.shift();
       if (item) {
          doSomething(item).then(doSmethingElse).then((x) => {
             result.push(x);
             next();
          });
       } else {
          resolve(result);
       }
   })();
});
jcubic
  • 61,973
  • 54
  • 229
  • 402