368

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];
Hidden
  • 3,895
  • 3
  • 11
  • 11

6 Answers6

385

Note:

This answer just covers the timing differences between await in series and Promise.all. Be sure to read @mikep's comprehensive answer that also covers the more important differences in error handling.


For the purposes of this answer I will be using some example methods:

  • res(ms) is a function that takes an integer of milliseconds and returns a promise that resolves after that many milliseconds.
  • rej(ms) is a function that takes an integer of milliseconds and returns a promise that rejects after that many milliseconds.

Calling res starts the timer. Using Promise.all to wait for a handful of delays will resolve after all the delays have finished, but remember they execute at the same time:

Example #1
const data = await Promise.all([res(3000), res(2000), res(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========O                     delay 3
//
// =============================O Promise.all

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }

  const data = await Promise.all([res(3000), res(2000), res(1000)])
  console.log(`Promise.all finished`, Date.now() - start)
}

example()

This means that Promise.all will resolve with the data from the inner promises after 3 seconds.

But, Promise.all has a "fail fast" behavior:

Example #2
const data = await Promise.all([res(3000), res(2000), rej(1000)])
//                              ^^^^^^^^^  ^^^^^^^^^  ^^^^^^^^^
//                               delay 1    delay 2    delay 3
//
// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =========X                     Promise.all

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const data = await Promise.all([res(3000), res(2000), rej(1000)])
  } catch (error) {
    console.log(`Promise.all finished`, Date.now() - start)
  }
}

example()

If you use async-await instead, you will have to wait for each promise to resolve sequentially, which may not be as efficient:

Example #3
const delay1 = res(3000)
const delay2 = res(2000)
const delay3 = rej(1000)

const data1 = await delay1
const data2 = await delay2
const data3 = await delay3

// ms ------1---------2---------3
// =============================O delay 1
// ===================O           delay 2
// =========X                     delay 3
//
// =============================X await

async function example() {
  const start = Date.now()
  let i = 0
  function res(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve()
        console.log(`res #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  function rej(n) {
    const id = ++i
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject()
        console.log(`rej #${id} called after ${n} milliseconds`, Date.now() - start)
      }, n)
    })
  }
  
  try {
    const delay1 = res(3000)
    const delay2 = res(2000)
    const delay3 = rej(1000)

    const data1 = await delay1
    const data2 = await delay2
    const data3 = await delay3
  } catch (error) {
    console.log(`await finished`, Date.now() - start)
  }
}

example()
Community
  • 1
  • 1
zzzzBov
  • 174,988
  • 54
  • 320
  • 367
  • 19
    So basically the difference is just the "fail fast" feature of Promise.all? – Matthew Mar 08 '18 at 05:23
  • 3
    So regarding the success case where everything is successful, there are no cases where `Promise.all()` will be faster than doing 3 `await` back to back, is that correct? So doing `Promise.all()` for performance reason is useless, except maybe for the fail-fast scenario? – HLP Aug 24 '18 at 18:09
  • 3
    @HenriLapierre, I've seen too many developers make the mistake of performing serial `await`s (i.e. `data1 = await thing1(); data2 = await thing2(); data3 = await thing3();`) thinking that they're running promises in parallel. So to answer your question, **if your promises have already been started they can't be made to resolve any faster**. I don't know why you would think that they could be sped up somehow by `Promise.all()`. – zzzzBov Aug 24 '18 at 18:25
  • Right, with your example `data1 = await thing1(); data2 = await thing2();`. It means that we don't start the second promise until the first one is resolved. If both are doing background action (i.e they can progress in parallel), is it better to do `promise1 = thing1(); promise2 = thing2()` and then `Promise.all([promise1, promise2])` ? Which is about identical to `promise1 = thing1(); promise2 = thing2(); data1 = await promise1; data2 = await promise2`. But there's still a gain in delaying the `await` until last minute. – HLP Aug 24 '18 at 20:44
  • 1
    In example #3, the total runtime until all awaits are done is 3000 + 2000 + 1000ms, isn't it? I.e., delay 2 starts when delay 1 has finished. – haggis Sep 04 '18 at 13:37
  • @haggis No, delay2 doesn't start after delay 1 has finished. It starts after delay 1 has been launched, but that's it, it doesn't wait for it to be finished. – Manuel Sep 24 '18 at 18:05
  • 7
    @mclzc In example #3 further code execution is halted until delay1 resolves. It's even in the text "If you use async-await instead, you will have to wait for each promise to resolve sequentially" – haggis Sep 25 '18 at 08:51
  • @haggis, `setTimeout` doesn't get halted during `await`. This topic was already [thoroughly covered earlier](https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await/45286517?noredirect=1#comment78492751_45286517) and you can see the actual behavior by running the examples in the code snippets. – zzzzBov Sep 25 '18 at 15:06
  • @haggis After delay1 resolves, delay2 and delay3 will resolve instantly because they were started way before. You don't have access to their values though, until delay1 resolves. You will have access to all the values of delay1, delay2 and delay3 after a grand total of 3 seconds. – Manuel Sep 28 '18 at 23:40
  • Yes, I overlooked that they all started before. – haggis Sep 29 '18 at 09:22
  • 1
    On Node, `const sleep = require('util').promisify(setTimeout)` then `await sleep(2000)`; – Brian H. Aug 05 '19 at 19:56
  • 5
    "*it may not be as efficient*" - and more importantly, cause `unhandledrejection` errors. You will never want to use this. Please add this to your answer. – Bergi Aug 14 '19 at 18:20
  • Beware that example #3 is really dangerous. You cannot catch rejections on delay2 and delay1 in this example. Either await in sequence, or await Promise.all. Has bitten me many times. Code like this should give a "compiler error" imho. – oldwizard Nov 18 '20 at 15:23
  • @oldwizard, this has already been discussed in the comment thread and is the specific reason why there is a large note at the top of my answer. The intent of my answer was to answer the original question of "Are there differences between `Promise.all` and multiple awaits" with "Yes, here's an example where they are different", _not_ to provide a comprehensive treatise on the proper engineering of asynchronous code. – zzzzBov Nov 18 '20 at 19:47
  • Great visualization. For me it was also useful to see an example where the promises are also created sequentially, so instead of `const delay1 = res(3000); ... await delay1` you just have `await res(3000)`. There's a question that deals with that well here: https://stackoverflow.com/questions/64328865/what-makes-async-await-statements-run-sequentially-vs-in-parallel-in-es6?noredirect=1&lq=1 – Charlie A Jan 03 '21 at 18:47
  • why they don't just create one async func with any parameters so we don't need to remember all the things? – kreamik Jul 19 '21 at 02:41
  • So I'm surprised to learn that sequential lines of await are in fact executed in parallel. How, then, do I execute something _only after_ all awaits have completed ? Wrap everything in an async function and await that ? – bbsimonbb Apr 26 '22 at 08:36
286

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 immediately 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 immediately 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 behaviour 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).

Audwin Oyong
  • 2,247
  • 3
  • 15
  • 32
mikep
  • 5,880
  • 2
  • 30
  • 37
  • 36
    this answer is better than the accepted answer because the currently accepted answer misses the very important topic of error handling – chrishiestand Aug 08 '19 at 22:17
  • 2
    No need to mention threads, it all runs in a single thread. [Concurrency is not Parallelism](https://www.youtube.com/watch?v=oV9rvDllKEg) – Kutalia Sep 08 '21 at 15:24
  • It is worthy as it own topic as error handling. Just wanna ask how the system generates these warnings, is there a global listener or sort of wrapper around promise rejection, or promise handled asynchronouisly? – Xiaoye Yang Mar 01 '22 at 18:31
  • `try { [await rejected1, await rejected2] } catch(err) { console.log(err) }` would catch the first promise (left to right) that failed, since we unwrap then in order, waiting (in order) if necessary. It would *not* lead to `UnhandledPromiseRejectedWarning`. – Capi Etheriel Jul 06 '22 at 22:47
  • What about wrapping each `await` in a `try/catch` block? Seems to me like the error is correct-- your promises _are_ unhandled. Or am I missing something about the order in which things happen? – sleighty Dec 08 '22 at 01:51
  • 1
    @sleighty topic is complex so you must provide short example what do you think about. This `try { let r1 = await task(1); let r2 = await task(2); } catch (err) {}` runs tasks in serial and handling works this way. This `try { let t1 = task(1); let t2 = task(2); let r1 = await t1; let r2 = await t2; } catch (err) {}` runs tasks in parallel but you catched nothing - handling is not working this way because try/catch is for sync code but omitting await before task call code fails asynchronously in background. All is in examples in my answer. – mikep Dec 08 '22 at 11:29
  • 1
    @mikep I see what you mean! Didn't know that it would fail asynchronously in the background. Thanks for the examples. – sleighty Jan 18 '23 at 23:48
59

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.

GavinBelson
  • 2,514
  • 25
  • 36
  • 13
    Where can I get a detailed explanation of what happens under the hood in snippets 1 and 2? I am so surprised that these have a different way to run as I was expecting behaviors to be the same. – Gregordy Mar 04 '20 at 06:19
  • 3
    @Gregordy yes it is surprising. I posted this answer to save coders new to async some headaches. It is all about when JS evaluates the await, this is why how you assign variables matters. In depth Async reading: https://blog.bitsrc.io/understanding-javascript-async-and-await-with-examples-a010b03926ea – GavinBelson Mar 04 '20 at 21:07
  • In snippet 1, you can handle errors gracefully with try..catch. In snippet 2, you have the problem with unhandled promise rejection mentioned by @mikep – Luke H Jul 08 '21 at 00:04
  • If you have only a handful of async functions or they are quite fast then the difference in completion time would be negligible. But if you have hundreds of them, not using Promise.all would be a great waste of resources as the event loop will process them sequentially (unless you need them be). Try building a web scraper in async/await you’ll see the problem – maksbd19 Aug 21 '21 at 00:15
10

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.

zpr
  • 2,886
  • 1
  • 18
  • 21
  • 9
    Actually, your fiddle doesn't address his question. There is a difference between calling `t1 = task1(); t2 = task2()` and **then** using `await` afterwards for both of them `result1 = await t1; result2 = await t2;` like in his question, as opposed to what you're testing which is using `await` on the original call like `result1 = await task1(); result2 = await task2();`. The code in his question does start all promises at once. The difference, like the answer shows, is that failures will get reported quicker with the `Promise.all` way. – BryanGrezeszak Apr 16 '18 at 02:05
  • Your answer is off topic such as @BryanGrezeszak commented. You should rather delete it to avoid misleading users. – mikep Jan 21 '19 at 14:09
  • 1
    its off topic. but this might help someone understand better, it helped me – some_groceries Mar 30 '21 at 12:10
  • fiddle was helpful! FYI, you passed ```000``` to the last ```Promise.delay``` inside your ```Promise.all```, but it didn't affect the results – J.E.C. Sep 14 '22 at 09:49
0

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.

Waleed Naveed
  • 2,293
  • 2
  • 29
  • 57
0

Just in case, in addition to the already awesome answers:

const rejectAt = 3;

// No worries. "3" is purely awesome, too! Just for the tiny example!

document.body.innerHTML = '';
o("// With 'Promise.all()':");

let a = Promise.all([
    test(1),
    test(2),
    test(3),
    test(4),
    test(5),
]).then(v => {
    o(`+ Look! We got all: ${v}`);
}).catch(e => {
    o(`x Oh! Got rejected with '${e}'`);
}).finally(() => {
    o("\n// With 'await':");

    async function test2() {
        try {
            r = [];
            r.push(await test(1));
            r.push(await test(2));
            r.push(await test(3));
            r.push(await test(4));
            r.push(await test(5));
            o(`+ Look! We got all: ${r.join(',')} // Twice as happy! ^^`);
        } catch (e) {
            o(`x Ah! Got rejected with '${e}'`);
        }
    }

    test2();
});

function test(v) {
    if (v === rejectAt) {
        o(`- Test ${v} (reject)`);
        return new Promise((undefined, reject) => reject(v));
    }

    o(`- Test ${v} (resolve)`);
    return new Promise((resolve, undefined) => resolve(v));
}

// ----------------------------------------

// Output
function o(value) {
    document.write(`${value}\n`);
}
body {
  white-space: pre;
  font-family: 'monospace';
}

A possible result:

// With 'Promise.all()':
- Test 1 (resolve)
- Test 2 (resolve)
- Test 3 (reject)
- Test 4 (resolve)
- Test 5 (resolve)
x Oh! Got rejected with '3'

// With 'await':
- Test 1 (resolve)
- Test 2 (resolve)
- Test 3 (reject)
x Ah! Got rejected with '3'
Artfaith
  • 1,183
  • 4
  • 19
  • 29