0

I am reducing an array of nested promises in the following way:

const promises = items.map((item, index) => {
    console.log(index);

    return doAsyncThing(item).then(() => {
       console.log(index);
    });
});

return promises.reduce((acc, p) => acc.then(() => p), Promise.resolve());

I want the console.log entries to print out

0 0 1 1 2 2

but instead they are printing out

0 1 2 2 1 0

How can I restructure my code so that the entirety of operation 0 is completed before operation 1, which is completed before option 2, etc...?

Prakash Sharma
  • 15,542
  • 6
  • 30
  • 37
sir_thursday
  • 5,270
  • 12
  • 64
  • 118
  • You need to only call `doAsyncThing()` after the previous call finishes (in your `reduce`) – SLaks Nov 14 '17 at 19:42
  • 1
    i,... don't understand why you are using reduce here instead of promise.all, which would preserve the order by default. note however that the indexes you are logging would still occur out of order, but the results in the promise.all .then callback will be in order. – Kevin B Nov 14 '17 at 19:45
  • You need to think clearly about what you are actually doing. Once you have a promise that needs to be resolved, there is no need to call `then` on that promise. Also the output you want seems to suggest that you want linear execution, not concurrent – smac89 Nov 14 '17 at 19:48
  • I don't want to use `Promise.all` because I read that order of completed promises is not guaranteed to be sequential? Is this not correct? And you're correct @smac89, I want linear execution. – sir_thursday Nov 14 '17 at 19:52
  • You've already sent the requests, so they're going to be sent all at the same time, and complete in a random order. You can use promise.all to allow that to continue while still getting the results in the expected order once they're all done. If you wanted to instead just send one request at a time, your .map would need to be replaced with a .reduce,. – Kevin B Nov 14 '17 at 19:53
  • Thanks for your answer @KevinB, the issue is that I also want the requests to be sent in sequential order. – sir_thursday Nov 14 '17 at 19:55
  • Then you'll want to remove your .map and just use reduce. – Kevin B Nov 14 '17 at 19:55
  • @KevinB I guess what OP wants is that the second promise should start only if the first promise is resolved. In that case `Promise.all` is not useful. – Prakash Sharma Nov 14 '17 at 19:57
  • @Prakashsharma eh, the OP hasn't mentioned anything about error handling. – Kevin B Nov 14 '17 at 19:57
  • This answer solves your problem: https://stackoverflow.com/a/41921099/400654 – Kevin B Nov 14 '17 at 19:59
  • or even better: https://stackoverflow.com/questions/21372320/how-to-chain-execution-of-array-of-functions-when-every-function-returns-deferre – Kevin B Nov 14 '17 at 20:02

1 Answers1

3

The Problem

The problem with your implementation is that all of the promises are created at the same time, and that will cause them to place their asynchronous actions in the event queue at that time. In your case, this is happening in the map function. Let's walk through this.

// your current code
const promises = items.map((item, index) => {
  console.log(index);       // this happens immediately and waits for nothing, which is why you see `0  1  2` in the console

  return doAsyncThing(item) // right here the async action is created
    .then(() => {           // this will run as soon as the async action is finished
      console.log(index);
    });
});

After this step, promises is already an array of promises, all of which have already queued up their asynchronous actions, not waiting for any others to be completed.

Solution

You have the right idea with the reduce, but by the time your reduce runs, all of the promises are already created, and will run in whatever order they finish. If you want to enforce order, you will need to use the reduce to create the promises initially:

const promises = items.reduce((acc, item, index) => {
  console.log(index) // still runs immediately
  return acc
    .then(() => doAsyncThing(item)) // creates the promise after acc finishes
    .then(() => console.log(index));    // runs after doAsyncThing finishes
}, Promise.resolve());

This will produce the output 0 1 2 0 1 2 in your console, but action 1 will still not be run until action 0 finishes, and action 2 will not be run until action 1 finishes. I hope this helps!

shamsup
  • 1,952
  • 14
  • 18