Reputation: 4305
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];
Upvotes: 409
Views: 362117
Reputation: 7438
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 is only faster in the failure scenario (when some task fails). And I ask, why not? If I have two async concurrent 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 failure scenario. But OK - in this first difference you can decide which alternative to use: Promise.all
vs. multiple await
.
But when considering error handling, you must use Promise.all
. It is not possible to correctly handle errors of async concurrent tasks triggered with multiple await
s. In the failure 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', e => {})
and process.on('rejectionHandled', e => {})
but this is not good practice. I have found many examples on the web that do not consider error handling for two or more async concurrent tasks at all, or consider it but in the wrong way - just using try
-catch
and hoping it will catch errors. It is almost impossible to find good practice in this.
TL;DR: Never use multiple await
for two or more async concurrent 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 is just a pretty way to use promises. Async code is written in sync style and we can avoid multiple then
s in promises.
Some people say that when using Promise.all
we cannot handle task errors separately, and that we can only handle the error from the first rejected promise (separate handling can be useful, for instance for logging). This is not a problem - see the ‘Addition’ heading at the bottom of this answer.
Consider this async task.
function task(id, duration, fail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (fail) {
reject(new Error(`Task ${id} failed!`));
} else {
resolve(`Task ${id} succeeded!`);
}
}, duration);
});
}
When you run tasks in the success scenario there is no difference between Promise.all
and multiple await
s. Both examples end with Task 1 succeeded! Task 2 succeeded!
after 5 seconds.
// Promise.all alternative
async function run() {
// run tasks concurrently
const t1 = task(1, 5000, false);
const t2 = task(2, 5000, false);
// wait for both results
const [r1, r2] = await Promise.all([t1, t2]);
console.log(r1, r2);
}
run();
// at the 5th second: Task 1 succeeded! Task 2 succeeded!
// multiple await alternative
async function run() {
// run tasks concurrently
const t1 = task(1, 5000, false);
const t2 = task(2, 5000, false);
// wait for each result sequentially
const r1 = await t1;
const r2 = await t2;
console.log(r1, r2);
}
run();
// at the 5th second: Task 1 succeeded! Task 2 succeeded!
However when you run task in the failure scenario, for instance if the first task takes 10 seconds and succeeds and the second task takes 5 seconds and fails, there are differences in the errors issued.
// Promise.all alternative
async function run() {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const [r1, r2] = await Promise.all([t1, t2]);
console.log(r1, r2);
}
run();
// at the 5th second: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// multiple await alternative
async function run() {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const r1 = await t1;
const r2 = await t2;
console.log(r1, r2);
}
run();
// at the 5th second: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at the 10th second: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
// at the 10th second: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
We should already notice here that we are doing something wrong when using multiple await
s sequentially. Let us try handling the errors:
// Promise.all alternative
async function run() {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const [r1, r2] = await Promise.all([t1, t2]);
console.log(r1, r2);
}
run().catch(e => {
console.error('Caught error', e);
});
// at the 5th second: 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
and try
-catch
) try { await run(); } catch (e) { console.error('Caught error', e); }
, but in this example it is not possible because we cannot 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):
// Promise.all alternative
async function run() {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const [r1, r2] = await Promise.all([t1, t2]);
console.log(r1, r2);
}
(async function () {
try {
await run();
} catch (e) {
console.error('Caught error', e);
}
})();
This is the only correct way to run two or more async concurrent tasks and handle errors. You should avoid the examples below.
// multiple await alternative
async function run() {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const r1 = await t1;
const r2 = await t2;
console.log(r1, r2);
}
run().catch(e => {
console.error('Caught error', e);
});
// at the 5th second: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at the 10th second: Caught error [Error: Task 2 failed!]
// at the 10th second: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
We see that the error for task 2 was not handled and later that it was caught. Misleading and still full of errors in console, it is still unusable this way. We 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(() => {
throw new Error();
}, 0);
}
try {
test();
} catch (e) { // this will never catch the error
console.error('Caught error', e);
}
Another poor example:
async function run() {
try {
const t1 = task(1, 10000, false);
const t2 = task(2, 5000, true);
const r1 = await t1;
const r2 = await t2;
console.log(r1, r2);
} catch (e) {
console.error('Caught inner error', e);
}
}
run().catch(e => {
console.error('Caught error', e);
});
// at the 5th second: UnhandledPromiseRejectionWarning: Error: Task 2 failed!
// at the 10th second: Caught inner error [Error: Task 2 failed!]
// at the 10th second: PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
Only two errors (3rd one is missing) but nothing is caught.
async function run() {
const t1 = task(1, 10000, true).catch(e => {
console.error('Task 1 failed!');
throw e;
});
const t2 = task(2, 5000, true).catch(e => {
console.error('Task 2 failed!');
throw e;
};
const [r1, r2] = await Promise.all([t1, t2]);
console.log(r1, r2);
}
run().catch(e => {
console.error('Run failed (does not matter which task)!');
});
// at the 5th second: Task 2 failed!
// at the 5th second: Run failed (does not matter which task)!
// at the 10th second: Task 1 failed!
Note that in this example I rejected both tasks to better demonstrate what happens (throw e
is used to fire the fail-fast error).
Upvotes: 330
Reputation: 179046
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:
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:
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:
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()
Upvotes: 414
Reputation: 1555
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'
Upvotes: 0
Reputation: 2784
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.
Upvotes: 68
Reputation: 2381
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.
Upvotes: 0