[javascript] Any difference between await Promise.all() and multiple await?

Is there any difference between:

const [result1, result2] = await Promise.all([task1(), task2()]);

and

const t1 = task1();
const t2 = task2();

const result1 = await t1;
const result2 = await t2;

and

const [t1, t2] = [task1(), task2()];
const [result1, result2] = [await t1, await t2];

This question is related to javascript async-await

The answer is


You can check for yourself.

In this fiddle, I ran a test to demonstrate the blocking nature of await, as opposed to Promise.all which will start all of the promises and while one is waiting it will go on with the others.


First difference - Fail Fast

I agree with @zzzzBov's answer, but the "fail fast" advantage of Promise.all is not the only difference. Some users in the comments have asked why using Promise.all is worth it when it's only faster in the negative scenario (when some task fails). And I ask, why not? If I have two independent async parallel tasks and the first one takes a very long time to resolve but the second is rejected in a very short time, why leave the user to wait for the longer call to finish to receive an error message? In real-life applications we must consider the negative scenario. But OK - in this first difference you can decide which alternative to use: Promise.all vs. multiple await.

Second difference - Error Handling

But when considering error handling, YOU MUST use Promise.all. It is not possible to correctly handle errors of async parallel tasks triggered with multiple awaits. In the negative scenario you will always end with UnhandledPromiseRejectionWarning and PromiseRejectionHandledWarning, regardless of where you use try/ catch. That is why Promise.all was designed. Of course someone could say that we can suppress those errors using process.on('unhandledRejection', err => {}) and process.on('rejectionHandled', err => {}) but this is not good practice. I've found many examples on the internet that do not consider error handling for two or more independent async parallel tasks at all, or consider it but in the wrong way - just using try/ catch and hoping it will catch errors. It's almost impossible to find good practice in this.

Summary

TL;DR: Never use multiple await for two or more independent async parallel tasks, because you will not be able to handle errors correctly. Always use Promise.all() for this use case.

Async/ await is not a replacement for Promises, it's just a pretty way to use promises. Async code is written in "sync style" and we can avoid multiple thens in promises.

Some people say that when using Promise.all() we can't handle task errors separately, and that we can only handle the error from the first rejected promise (separate handling can be useful e.g. for logging). This is not a problem - see "Addition" heading at the bottom of this answer.

Examples

Consider this async task...

const task = function(taskNum, seconds, negativeScenario) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      if (negativeScenario)
        reject(new Error('Task ' + taskNum + ' failed!'));
      else
        resolve('Task ' + taskNum + ' succeed!');
    }, seconds * 1000)
  });
};

When you run tasks in the positive scenario there is no difference between Promise.all and multiple awaits. Both examples end with Task 1 succeed! Task 2 succeed! after 5 seconds.

// Promise.all alternative
const run = async function() {
  // tasks run immediate in parallel and wait for both results
  let [r1, r2] = await Promise.all([
    task(1, 5, false),
    task(2, 5, false)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!
// multiple await alternative
const run = async function() {
  // tasks run immediate in parallel
  let t1 = task(1, 5, false);
  let t2 = task(2, 5, false);
  // wait for both results
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: Task 1 succeed! Task 2 succeed!

However, when the first task takes 10 seconds and succeeds, and the second task takes 5 seconds but fails, there are differences in the errors issued.

// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
      task(1, 10, false),
      task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};
run();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!

We should already notice here that we are doing something wrong when using multiple awaits in parallel. Let's try handling the errors:

// Promise.all alternative
const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, false),
    task(2, 5, true)
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: Caught error Error: Task 2 failed!

As you can see, to successfully handle errors, we need to add just one catch to the run function and add code with catch logic into the callback. We do not need to handle errors inside the run function because async functions do this automatically - promise rejection of the task function causes rejection of the run function.

To avoid a callback we can use "sync style" (async/ await + try/ catch)
try { await run(); } catch(err) { }
but in this example it's not possible, because we can't use await in the main thread - it can only be used in async functions (because nobody wants to block main thread). To test if handling works in "sync style" we can call the run function from another async function or use an IIFE (Immediately Invoked Function Expression: MDN):

(async function() { 
  try { 
    await run(); 
  } catch(err) { 
    console.log('Caught error', err); 
  }
})();

This is the only correct way to run two or more async parallel tasks and handle errors. You should avoid the examples below.

Bad Examples

// multiple await alternative
const run = async function() {
  let t1 = task(1, 10, false);
  let t2 = task(2, 5, true);
  let r1 = await t1;
  let r2 = await t2;
  console.log(r1 + ' ' + r2);
};

We can try to handle errors in the code above in several ways...

try { run(); } catch(err) { console.log('Caught error', err); };
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled 

... nothing got caught because it handles sync code but run is async.

run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

... huh? We see firstly that the error for task 2 was not handled and later that it was caught. Misleading and still full of errors in console, it's still unusable this way.

(async function() { try { await run(); } catch(err) { console.log('Caught error', err); }; })();
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: Caught error Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

... the same as above. User @Qwerty in his deleted answer asked about this strange behavior where an error seems to be caught but are also unhandled. We catch error the because run() is rejected on the line with the await keyword and can be caught using try/ catch when calling run(). We also get an unhandled error because we are calling an async task function synchronously (without the await keyword), and this task runs and fails outside the run() function.
It is similar to when we are not able to handle errors by try/ catch when calling some sync function which calls setTimeout:

function test() {
  setTimeout(function() { 
    console.log(causesError); 
    }, 0);
}; 
try { 
  test(); 
} catch(e) { 
  /* this will never catch error */ 
}`.

Another poor example:

const run = async function() {
  try {
    let t1 = task(1, 10, false);
    let t2 = task(2, 5, true);
    let r1 = await t1;
    let r2 = await t2;
  }
  catch (err) {
    return new Error(err);
  }
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Caught error', err); });
// at 5th sec: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at 10th sec: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

... "only" two errors (3rd one is missing) but nothing is caught.

Addition (handling separate task errors and also first-fail error)

const run = async function() {
  let [r1, r2] = await Promise.all([
    task(1, 10, true).catch(err => { console.log('Task 1 failed!'); throw err; }),
    task(2, 5, true).catch(err => { console.log('Task 2 failed!'); throw err; })
  ]);
  console.log(r1 + ' ' + r2);
};
run().catch(err => { console.log('Run failed (does not matter which task)!'); });
// at 5th sec: Task 2 failed!
// at 5th sec: Run failed (does not matter which task)!
// at 10th sec: Task 1 failed!

... note that in this example I rejected both tasks to better demonstrate what happens (throw err is used to fire final error).


In case of await Promise.all([task1(), task2()]); "task1()" and "task2()" will run parallel and will wait until both promises are completed (either resolved or rejected). Whereas in case of

const result1 = await t1;
const result2 = await t2;

t2 will only run after t1 has finished execution (has been resolved or rejected). Both t1 and t2 will not run parallel.


Generally, using Promise.all() runs requests "async" in parallel. Using await can run in parallel OR be "sync" blocking.

test1 and test2 functions below show how await can run async or sync.

test3 shows Promise.all() that is async.

jsfiddle with timed results - open browser console to see test results

Sync behavior. Does NOT run in parallel, takes ~1800ms:

const test1 = async () => {
  const delay1 = await Promise.delay(600); //runs 1st
  const delay2 = await Promise.delay(600); //waits 600 for delay1 to run
  const delay3 = await Promise.delay(600); //waits 600 more for delay2 to run
};

Async behavior. Runs in paralel, takes ~600ms:

const test2 = async () => {
  const delay1 = Promise.delay(600);
  const delay2 = Promise.delay(600);
  const delay3 = Promise.delay(600);
  const data1 = await delay1;
  const data2 = await delay2;
  const data3 = await delay3; //runs all delays simultaneously
}

Async behavior. Runs in parallel, takes ~600ms:

const test3 = async () => {
  await Promise.all([
  Promise.delay(600), 
  Promise.delay(600), 
  Promise.delay(600)]); //runs all delays simultaneously
};

TLDR; If you are using Promise.all() it will also "fast-fail" - stop running at the time of the first failure of any of the included functions.