Search code examples
node.jstypescriptasync-await

Awaiting two promises in try/catch block results in "Unhandled promise rejection"


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.

  • Why is that?
  • How do I best run two or multiple network requests in parallel otherwise with elegant code?
  • Why does Typescript not warn me that the catch-block will not catch rejections?
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)

Solution

  • 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!"

    Click for gif animation.

    Chrome output after running the code