Node.js Promise reject use case survey

Today, Node.js Project, an impact project of the OpenJS Foundation, handles unhandled rejections by emitting a deprecation warning to stderr. The warning shows the stack where the rejection happened, and states that in future Node.js versions unhandled rejections will result in Node.js exiting with non-zero status code. We intend to remove the deprecation warning, replacing it with a stable behavior which might be different from the one described on the deprecation warning. We’re running a survey to better understand how Node.js users are using Promises and how they are dealing with unhandled rejections today, so we can make an informed decision on how to move forward.

What is a Promise rejection?

A Promise rejection indicates that something went wrong while executing a Promise or an async function. Rejections can occur in several situations: throwing inside an async function or a Promise executor/then/catch/finally callback, when calling the reject callback of an executor, or when calling Promise.reject.

Adding the async keyword to a function will turn any exceptions thrown inside that function (or any throw propagated from other functions called within it) into a rejection. The same happens when refactoring callback based code that throws into async functions / Promises. Below is an example of a callback based code refactored to Promises where exceptions become rejections:

What is an unhandled rejection?

There are two ways to handle rejections: by attaching a .catch handler to it, or by awaiting on the promise within a try/catch block. In both cases, the
handling of the rejection (the execution of the callback passed to .catch, or
the execution of the catch {} block) will happen in a future turn of the
event loop.

Promises are designed so that attaching handlers or awaiting can be done at any point in time, from when the Promise was created (possibly while it’s still pending), to right before the program finishes execution.

A rejection is considered unhandled from the point it happens until the point where a handler is attached to the Promise or the Promise is awaited within a catch {} block. Below are a few examples of handled and unhandled rejections.

As we can see in the examples, an unhandled rejection might be handled in the future, like example 5, but it might also stay unhandled forever (like example 4).

Certain unhandled rejections may in rare cases leave your application in a non-deterministic and unsafe state, whether it’s internal application state (including memory leaks), external resources used by your application (say, file handles or database connections), or external state (say, consistency of data in a database). As an example, the following server is not sending a response back to the client, causing a socket leak and a possible Denial of Service attack:

Mitigating the issue

There are a few ways developers can mitigate this issue. Node.js provides
an unhandled rejections listener (process.on('unhandledRejection'), which
developers can use to programmatically decide if a rejection is potentially
leaking resources or leaving the application on a non-deterministic state.
Using the server example above, a developer could mitigate the issue as follow:

The process will crash, but that might be preferrable to leaking resources in this case. In other cases, the listener might attempt to gracefully recover or to gracefully shut down by first responding to any requests still in progress, and only crash if it can’t respond to pending requests.

Using the listeners requires changing the application code, and the behavior can be overridden by any dependency. To circumvent those limitations, a new flag (--unhandled-rejection=[mode]) was added to Node.js. This flag supports five different modes:

  • strict: raise an uncaught exception similar to throw new Error() that is not caught. unhandledRejection listeners do not prevent raising the exception
  • throw: raise an uncaught exception similar to throw new Error() that is not caught. unhandledRejection listeners take precedence and prevent raising the exception
  • warn: outputs a warning as soon as possible. Continues running after the warning is emitted. If the process exits and no status code was set, the process exits with a success code. This is similar to what browser consoles do
  • warn-with-error-code: outputs a warning as soon as possible. Continues running after the warning is emitted. If the process exits and no status code was set, the process exits with an error code
  • none: do nothing

For all the modes, the action (raise an exception output a warning) will happen on nextTick. strict mode circumvents the problem stated above since it prevents dependencies from overriding the unhandleRejection listener. warn is similar to the current behavior but without a deprecation warning.

Either the listener and flag can be used to mitigate issues with unhandled rejections.

Defining a default for the project

As described above, unhandled rejections can potentially leak resources, but they can also be harmless and even expected. Because of that, there has been ongoing discussion in the project for the past few years on what the default behavior should be. That’s a polarizing issue and therefore no consensus was reached so far. The TSC will vote on that issue in a few weeks, but before we vote we would like to better understand how developers are using Promises on Node.js today and what they think the default should be.

To help us decide on a default, please complete our survey: https://www.surveymonkey.com/r/FTJM7YD

Node.js is a collaborative open source project dedicated to building and supporting the Node.js platform. https://nodejs.org/en/