Home > Software engineering >  Node.JS: unhandledRejection with async function calls that are not awaited
Node.JS: unhandledRejection with async function calls that are not awaited

Time:12-14

Edit: in summary this issue is regarding unhandledRejection in node.js. I didn't expect a promise that was not awaited (but later fails) to crash the program - because I didn't realize that unhandledRejection was being thrown & causing node.js to exit. Refreshing my understanding of unhandledRejection I now realize that any promise anywhere in the program that is rejected without a catch() statement will throw unhandledRejection and exit the node.js process on default. Which doesn't make too much sense to me, but at least I understand why it's happening.

In the following code example, thisFailsLater initially returns successfully, but then throws an error some time later.

This results in runTest() passing the try/catch and moving on to the next await.

However, while waiting for the next await call, thisFailsLater then throws an error, which causes Node.JS to exit:

% node t.js 
Throwing error in 2 seconds
thisFailsLater returned successfully
t.js:4
  throw new Error('thrown error in thisFails()')
        ^

Error: thrown error in thisFails()
    at thisFails (t.js:4:9)

This is something I didn't expect: it seems that an async function can initially return successfully, but then any async function called but not awaited within the function that throws after returning will then crash the Node.JS process.

Am I correct in how this works? And if so, how can I prevent Node.JS from exiting when thisFailsLater() throws 2 seconds after returning successfully? This completely breaks my understanding of how Promises work (can only be resolved or errored once).

Error reproduction:

async function thisFails() {
  console.log('Throwing error in 2 seconds')
  await new Promise((resolve) => setTimeout(resolve, 2e3));
  throw new Error('thrown error in thisFails()')
}

async function thisFailsLater() {
  try {
    // NOTE: intentionally NOT awaiting this call here.
    thisFails()
  } catch (err) {
    console.log('thisFailsLater caught error', err)
  }
}

async function runTest() {
  try {
    await thisFailsLater()
  } catch (err) {
    console.error('runTest caught error', err)
  }
  console.log('thisFailsLater returned successfully')
  await new Promise((resolve) => setTimeout(resolve, 3e3));
}

runTest().then(() => {
  process.exit(0)
}).catch((err) => {
  // NOTE: the error that Node.JS crashes with is NOT logged here!
  console.error('Caught error in main()', err)
  process.exit(1)
})

CodePudding user response:

It seems that an async function can initially return successfully, but then any async function called but not awaited within the function that throws after returning will then crash the Node.JS process.

Am I correct in how this works?

Yes. But this is not unique to async functions - any function can do that in node.js:

function failLater() {
    setTimeout(() => {
        throw new Error("crash!");
    }, 1000);
    return "success"
}

And if so, how can I prevent Node.JS from exiting when thisFailsLater() throws 2 seconds after returning successfully?

Just don't do that.™

Do not let functions cause exceptions asynchronously.

To prevent nodejs from exiting, you can hook on the global error and unhandledrejection events, but those are meant for logging - your application already has suffered from an unrecoverable error.

This completely breaks my understanding of how Promises work (can only be resolved or errored once).

It's not related to promises, the promise does settle only once, when the body of the async function concludes evaluating. The thisFails() is a second, separate promise - independent from the flow of execution since you didn't await it.

CodePudding user response:

The problem occurs here:

async function thisFailsLater() {
  try {
    // NOTE: intentionally NOT awaiting this call here.
    thisFails()
  } catch (err) {
    console.log('thisFailsLater caught error', err)
  }
}

When you don't await the promise (which rejects), you fail to handle the rejection. One of the things that the await keyword does is "convert" a rejected promise into an exception in the async context — from the MDN documentation for await:

If the promise is rejected, the await expression throws the rejected value. The function containing the await expression will appear in the stack trace of the error. Otherwise, if the rejected promise is not awaited or is immediately returned, the caller function will not appear in the stack trace.

Because you don't await the rejected promise, it can't be caught in your try block, converted to an exception, and passed as the argument to the catch clause during the transition of control flow. If you don't await the promise, but want to handle the rejection, you must use the .catch() method promise syntax:

function thisFailsLater() {
  return thisFails().catch(exception => console.log('thisFailsLater caught error', exception));
}
  • Related