Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)
like image 461
oldwizard Avatar asked May 05 '20 14:05

oldwizard


2 Answers

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 the statement awaited first will end up in a promise.then callback function.

So, const b = await bPromise will not run before const a has arrived (after 200ms). bPromise fails 100ms sooner.

This is not to say that async/await does not pick up on the error or attach your catch block (as promise.catch(...)) 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 de catch clause does work, but the async function does not bother to attach it to bPromise until after at least 200ms. 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 100ms). 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

like image 197
JJWesterkamp Avatar answered Sep 22 '22 14:09

JJWesterkamp


A nice way to wait for several Promises to resolve to use the Promise.all function. It expects an Array of Promises, and produces a Promise that resolves to an Array containing the values that the individual Promises resolved to. Furthermore, it only resolves after the last Promise resolves. If any of its input Promises rejects, then the entire Promise.all expression rejects as well. It effectively "runs" all of its input processes "at the same time", emulating the classic "fork-join" pattern.

The reason you are getting that error is due to the fact that your timeout process is started as soon as you define those Promises, and not when you await them, several lines later. You can see this if you log some text in the definition of one of the Promises, and then log something else before your await expressions:

async function testMultipleAwait() {
  try {
    const aPromise = new Promise((resolve) => {
      console.log('starting timeout');
      setTimeout(() => resolve('a'), 200);
    });

    const bPromise = new Promise((_, reject) => {
      setTimeout(() => reject('b'), 100);
    });

    console.log('awaiting');
    const a = await aPromise;
    const b = await bPromise;
  } catch (e) {
    console.log('Caught error', e);
  }
}

testMultipleAwait();

// logs:
// "starting timeout"
// "awaiting"

To fix this immediate issue, you could convert those Promises to functions, and then call them, or await them "immediately":

// Option 1
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();
// Option 2
async function testMultipleAwait() {
  try {
    await new Promise(resolve => {
      setTimeout(() => resolve("a"), 200);
    });

    await new Promise((_, reject) => {
      setTimeout(() => reject("b"), 100);
    });
  } catch (e) {
    console.log("Caught error", e);
  }
}

testMultipleAwait();

Bringing this all together, to have them run in parallel, you could try something like this:

async function testMultipleAwait() {
  try {
    await Promise.all([
      new Promise(resolve => {
        setTimeout(() => resolve("a"), 200);
      }),
      new Promise((_, reject) => {
        setTimeout(() => reject("b"), 100);
      })
    ]);
  } catch (e) {
    console.log("Caught error", e);
  }
}

testMultipleAwait();
like image 25
William Lewis Avatar answered Sep 19 '22 14:09

William Lewis