I want to await two promises that run in parallel. I do not want to await each promise serially (which works but is slower).
For that reason I thought I could create two promises first to get them rolling, say two network requests, then await them and be able to catch errors in a catch block. That assumption seems to be incorrect as I get a warning when running this example code.
async function testMultipleAwait() {
try {
const aPromise = new Promise((resolve) => {
setTimeout(() => resolve('a'), 200);
});
const bPromise = new Promise((_, reject) => {
setTimeout(() => reject('b'), 100);
});
const a = await aPromise;
const b = await bPromise;
} catch (e) {
console.log('Caught error', e);
}
}
testMultipleAwait();
Does NOT result in "Caught error" output, instead I get
tsc test-try-catch-await.ts && node test-try-catch-await.js
(node:31755) UnhandledPromiseRejectionWarning: b
(node:31755) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:31755) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Caught error b
(node:31755) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
I think I know what the issue is. Even though the promises kick-off in a concurrent fashion, you are still awaiting aPromise
and bPromise
sequentially:
const a = await aPromise; // First await this...
const b = await bPromise; // and then start to await this
This is not so much of a problem when both promises fulfill. It would keep jou waiting about as much time as
Promise.all
would, and then happily continue. That is why this problem is not so obvious at all...
It's important to know that under the hood this try
-catch
is converted to a promise, due to async
-await
. That means that whatever comes after the statement awaited first will end up in a then
callback function.
So, const b = await bPromise
will not run before const a
has arrived (after 200 ms). bPromise
fails 100 ms sooner.
This is not to say that async
-await
does not pick up on the error or attach your catch
block altogether. After all, there is terminal output of both the node warning and your catch
handler:
tsc test-try-catch-await.ts && node test-try-catch-await.js
1 first node sees the error > (node:31755) UnhandledPromiseRejectionWarning: b
2 (node:31755) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
3 (node:31755) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
4 and then your catch handler > Caught error b
5 (node:31755) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
So the catch
clause does work, but the async
function does not bother to attach it to bPromise
until after at least 200 ms. Line 5 seems to confirm this:
PromiseRejectionHandledWarning: Promise rejection was handled asynchronously.
Rejection errors get thrown as soon as the microtask queue is empty.
The promise rejection was handled, but Node thinks you're too late. You can fix this issue by using Promise.all
. This way you await
once and your async
function will catch every potential error first.
// Everything just as it is.. and then:
const [a, b] = await Promise.all([
aPromise,
bPromise,
]);
Because I was curious I entered your code in the Chrome console to see what would happen. An error log pops up for a very short period (I'm guessing 100 ms). Looking at this output you can just hear Chrome saying:
"Ah wait! It is being caught after all. Here's the message!"