1

This is a basic question, but i couldn't find the answer to it anywhere.

We have two approaches:

// consider someFunction1() and someFunction2() as functions that returns Promises

Approach #1:
return [await someFunction1(), await someFunction2()]

Approach #2:
return await Promise.all([someFunction1(), someFunction2()])

My Team Leader said that both approaches ended up in the same solution (both functions executting in parallel). But, from my knowledge, the first approach would await someFunction1() to resolve and then would execute someFunction2.

So that's the question, is it really the same, or are there any performance improvements on second approach? Proofs are very welcome!

Alexander Santos
  • 1,458
  • 11
  • 22
  • 7
    "*My Team Leader said that both approaches ended up in the same solution (both functions executting in parallel).*" that is incorrect "*But, from my knowledge, the first approach would await someFunction1() to resolve and then would execute someFunction2.*" that is correct. – VLAZ Jun 14 '21 at 14:43
  • 1
    ^^ What they said! They are absolutely not the same thing. Easy enough to demonstrate - would you like me to? – Jamiec Jun 14 '21 at 14:50
  • If you don't mind, i would like it. Would be good, so i can use it as example to show him. – Alexander Santos Jun 14 '21 at 14:53
  • 1
    `const promise1 = someFunction1(), promise2 = someFunction2();` and then `return [await promise1, await promise2];` this would be comparable to `Promise.all()`. Here, the first `await` only defers/delays the "then"-part of the second promise, not it's creation. – Thomas Jun 14 '21 at 15:03

5 Answers5

7

No, you should not accept that:

return [await someFunction1(), await someFunction2()];

Is the same as:

return await Promise.all([someFunction1(), someFunction2()]);

I should also note that await in the above return await is not needed. Check out this blog post to learn more.

They are different!


The first approach (sequential)

Let's determine the difference by inspecting how each of the two alternatives works.

[await someFunction1(), await someFunction2()];

Here, in an async context, we create an array literal. Note that someFunction1 is called (a function which probably returns a new promise each time it gets called).

So, when you call someFunction1, a new promise is returned, which then "locks" the async context because the preceding await.

In a nutshell, the await someFunction1() "blocks" the array initialization until the returned promise gets settled (by getting resolved or rejected).

The same process is repeated to someFunction2.

Note that, in this first approach, the two promises are awaited in sequence. There is, therefore, no similarity with the approach that uses Promise.all. Let's see why.

The second approach (non-sequential)

Promise.all([someFunction1(), someFunction2()])

When you apply Promise.all, it expects an iterable of promises. It waits for all the promises you give to resolve before returns a new array of resolved values, but don't wait each promise resolve until waiting another one. In essence, it awaits all the promises at the same time, so it is a kind of "non-sequential". As JavaScript is single-threaded, you cannot tell this "parallel", but is very similar in the behavior point of view.

So, when you pass this array:

[someFunction1(), someFunction2()]

You are actually passing an array of promises (which are returned from the functions). Something like:

[Promise<...>, Promise<...>]

Note that the promises are being created outside Promise.all.

So you are, in fact, passing an array of promises to Promise.all. When both of them gets resolved, the Promise.all returns the array of resolved values. I won't explain in all details how Promise.all works, for that, I suggest you checking out the documentation.

You can replicate this "non-sequential" approach by creating the promises before using the await. Like so:

const promise1 = someFunction1();
const promise2 = someFunction2();
return [await promise1, await promise2];

While promise1 is being waited, promise2 is already running (as it was created before the first await), so the behavior is similar to Promise.all's.

Luiz Felipe
  • 869
  • 12
  • 21
  • 2
    `return [await promise1, await promise2];` is *not* the same as `Approach2`. It's *similar* but only if the promises resolve successfully. It can lead to uncaught promise rejections if one fails. https://jsbin.com/pojumujoqe/1/edit?js,console,output EDIT: see also [Any difference between await Promise.all() and multiple await?](https://stackoverflow.com/q/45285129) – VLAZ Jun 14 '21 at 15:14
  • I think this was just a misunderstanding, he tried to say `No, you should not accept that is the same as ` – Alexander Santos Jun 14 '21 at 15:18
  • 1
    @VLAZ, I said: "You can replicate this "non-sequential" approach [...]", **not** "You can replicate the `Promise.all` behavior [...]". I am well aware of the differences, but I don't think they enter in the scope of the question. I might edit it later to make this more clear, though. Thanks for the heads up, nonetheless. :) Another difference, of course, is that `[await p1, await p2]` does not short circuit, unlike `Promise.all([p1, p2])` but, again, I don't think this is about what the question asks. – Luiz Felipe Jun 14 '21 at 15:23
  • Check a complete guide here: https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await – Luan Persini Jan 26 '23 at 18:25
4

My Team Leader said that both approaches ended up in the same solution (both functions executting in parallel).

That is incorrect.

But, from my knowledge, the first approach would await someFunction1() to resolve and then would execute someFunction2.

That is correct.

Here is a demonstration

Approach 1:

const delay = (ms, value) =>
  new Promise(resolve => setTimeout(resolve, ms, value));
  
async function Approach1() {
  return [await someFunction1(), await someFunction2()];
}
  
async function someFunction1() {
  const result = await delay(800, "hello");
  console.log(result);
  return result;
}

async function someFunction2() {
  const result = await delay(400, "world");
  console.log(result);
  return result;
}

async function main() {
  const start = new Date();
  const result = await Approach1();
  const totalTime = new Date() - start;
  console.log(`result: ${result}
    total time: ${totalTime}`);
}

main();

Result is:

hello
world
result: hello,world
    total time: 1205

Which means that someFunction1 runs to completion first and then someFunction2 is executed. It is sequential

Approach 2:

const delay = (ms, value) =>
  new Promise(resolve => setTimeout(resolve, ms, value));
  
async function Approach2() {
  return await Promise.all([someFunction1(), someFunction2()]);
}
  
async function someFunction1() {
  const result = await delay(800, "hello");
  console.log(result);
  return result;
}

async function someFunction2() {
  const result = await delay(400, "world");
  console.log(result);
  return result;
}

async function main() {
  const start = new Date();
  const result = await Approach2();
  const totalTime = new Date() - start;
  console.log(`result: ${result}
    total time: ${totalTime}`);
}

main();

Result is:

world
hello
result: hello,world
    total time: 803

Which means that someFunction2 finishes before someFunction1. The two are parallel.

Tushar Shahi
  • 16,452
  • 1
  • 18
  • 39
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • 1
    Best answer so far, but could you please remove this `async`/`await` in `Approach2`? It's not needed at all (you can compare it to your `delay` function). – ulou Jun 14 '21 at 15:01
  • 1
    @ulou I used the code from OP. I prefer code to be representative to a question, even if redundant. Makes it easy to correlate it to the information in the question. – VLAZ Jun 14 '21 at 15:03
1

Easy to see the difference

    function createTimer(ms, id) {
      console.log(`id: ${id} started ${new Date()}`);
      return new Promise((res, rej) => {
        setTimeout( () => {
          console.log(`id: ${id} finished ${new Date()}`);
          res(id);      
        }, ms );
      });
    }

    (async function() {

      var result1 = [await createTimer(5000, '1'), await createTimer(5000, '2')];
      var result2 = await Promise.all([createTimer(5000, '3'), createTimer(5000, '4')]);

      console.log(result1);
      console.log(result2);

    })();

The first one starts 1 and when 1 finishes it starts 2. The second one starts 3 and 4 at almost the very same moment.

Wiktor Zychla
  • 47,367
  • 6
  • 74
  • 106
1

MDN documentation for Promise.all() states that

This method can be useful for aggregating the results of multiple promises. It is typically used when there are multiple related asynchronous tasks that the overall code relies on to work successfully — all of whom we want to fulfill before the code execution continues.

While it isn't explicit, you can await Promise.all to track multiple promises. Only when all promises are resolved will the code execution continue.

The other approach you mention of capturing separate asynchronous tasks in an array is not the same due to how await operates.

An await splits execution flow, allowing the caller of the async function to resume execution. After the await defers the continuation of the async function, execution of subsequent statements ensues. If this await is the last expression executed by its function, execution continues by returning to the function's caller a pending Promise for completion of the await's function and resuming execution of that caller.

So, each await will pause execution before resuming. No need for a demonstration.

GenericUser
  • 3,003
  • 1
  • 11
  • 17
  • 1
    "*While it isn't explicit, the intention is that Promise.all will run all the asynchronous tasks simultaneously*" this is misleading. `Promise.all` doesn't *run* tasks. Creating a promise will start a task. `Promise.all` will just wait for all of them to complete, at the point when you call `Promise.all` the task *is already running*. This is very important and very often misunderstood. Promises are only a notification mechanism for the state of an async operation (finished/failed/not yet ready). You don't "start" or "stop" an async task using the promise. – VLAZ Jun 14 '21 at 15:00
  • 1
    Adjusted for clarity. – GenericUser Jun 14 '21 at 15:08
1

If you start with a function which simulates doing some work which outputs at stages of that work but takes some time. eg

function someFunction1(){
   return new Promise(resolve => {
       let i = 0;
       const intervalId = setInterval(() => {
          i++;
          console.log("someFunction1", i);
          if(i == 5){
            clearInterval(intervalId)
            resolve(1); 
          }
       }, 1000);
   });
}

And then you duplicate that with a second, similar, method. You plug your 2 methods in and you see that the one using Promise.all does it in parallel but the one using 2 await calls does it in series.

Parallel

function someFunction1(){
   return new Promise(resolve => {
       let i = 0;
       const intervalId = setInterval(() => {
          i++;
          console.log("someFunction1", i);
          if(i == 5){
            clearInterval(intervalId)
            resolve(1); 
          }
       }, 1000);
   });
}

function someFunction2(){
   return new Promise(resolve => {
       let i = 0;
       const intervalId = setInterval(() => {
          i++;
          console.log("someFunction2", i);
          if(i == 5){
            clearInterval(intervalId)
            resolve(2); 
          }
       }, 1000);
   });
}

(async function(){
   const result = await Promise.all([someFunction1(),someFunction2()]);
   console.log("result",result);
})();

Series

function someFunction1(){
   return new Promise(resolve => {
       let i = 0;
       const intervalId = setInterval(() => {
          i++;
          console.log("someFunction1", i);
          if(i == 5){
            clearInterval(intervalId)
            resolve(1); 
          }
       }, 1000);
   });
}

function someFunction2(){
   return new Promise(resolve => {
       let i = 0;
       const intervalId = setInterval(() => {
          i++;
          console.log("someFunction2", i);
          if(i == 5){
            clearInterval(intervalId)
            resolve(2); 
          }
       }, 1000);
   });
}

(async function(){
   const result = [await someFunction1(),await someFunction2()];
   console.log("result",result);
})();

Both give the exact same result but getting there is very different.

Jamiec
  • 133,658
  • 13
  • 134
  • 193