Making Promises safer in Node.js
Promises are powerful, but beware of pitfalls
Promises (available from Node.js v4.0.0 onwards) can be a powerful choice for a project, but before buying into them there are some pitfalls to be aware of.
With EventEmitter
, and anything built on top of it such as streams, developers are used to Node.js exiting if an error
event is emitted without any listener to handle it. In this case, EventEmitter
causes the process to emit a global uncaughtException
event. This event is emitted by process
, in general, when any unhandled exceptions occur.
A simple example:
Running this code will output Error: ENOENT: no such file or directory, open 'non-existent-file.md'
. This is because fs.createReadStream
returns a stream.ReadStream
instance. In turn, stream.ReadStream
is an instance of events.EventEmitter
, therefore uncaughtException
is emitted.
Internally to Node, when there's a fatal exception V8 calls an onMessage
callback function in the C++ layer. This function is provided in src/node.cc and calls another C++ function ( FatalException ), which in turn calls a JavaScript function ( process._fatalException ) that is created in lib/internal/boostrap_node.js
, which finally calls process.emit('uncaughtException')
when the error goes unhandled.
Thrown errors are also emitted because of integration between EventEmitter
and V8:
Currently, Node.js gives unhandled promise rejections a little more leeway. If a rejection happens and there's no .reject(fn)
handler, the runtime prints this error to the console without crashing:
In order to use promises successfully, a rejection handler ( .catch(handleFn)
) should always be used, and should always be attached synchronously.
The Importance of Handling Rejections
Oftentimes any extra logic for handling errors in an application is seen as unnecessary, but properly dealing with exceptions is crucial for any application's security, efficiency, and performance. Many asynchronous operations leave file descriptors lying around or use a significant amount of RAM, and it's important to clean up when they fail in order to avoid memory leaks and other denial-of-service situations.
Take this example server:
If this server receives a request for /
, it will respond and clean up the buffer as expected. For all other requests, two things are noted:
- There will be a warning in the console:
(node:32236) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: Rule for /blog not found
- The requests don't clean up after themselves, so more and more RAM is used. Unless the path is
/
,generateReply()
returnsPromise.reject(...)
and the request handler has no.catch(...)
so clean up is never performed.
The behaviour can be reproduced by testing the server with ApacheBench ( ab
):
In order to fix these kinds of errors, add a .catch()
call to the promise and handle the rejection:
Future-Proofing using make-promises-safe
Node.js versions 6 and 8 will continue to chug along following an unhandled promise rejection. In future versions, unhandled rejections will cause the Node.js process to terminate, as per DEP0018 .
The best way to ensure that an application is future-proofed is to emulate Node.js's future behaviour today. Matteo Collina's make-promises-safe module binds an event listener to the global uncaughtRejection
event, causing any unhandled promise rejections to terminate the Node.js process. This is crucial because even when a developer knows to always use .catch()
, it is easy to forget to add it every time a promise is used.
Installing make-promises-safe
To install the module, use npm install make-promises-safe --save
, which will also make it a dependency of the project.
Usage
Using make-promises-safe
is as simple as requiring it in the application's entry script.
The application's code, along with any external modules, will be bound to this new behaviour. To test how the application fares with this behaviour, consider running unit tests; any new regressions most likely point toward promises without any catch()
handler.
Conclusion
Node.js is moving towards treating unhandled promise rejections similarly to uncaughtException
errors in the future. Soon, Node's behaviour will be to terminate with a stack trace whenever an unhandled promise rejection occurs. Using the make-promises-safe
module, developers can use that behaviour today. This promotes best practices by requiring unfulfilled promises to be handled with .catch()
.
Promises are number 8 on our list of features in our article about the top 10 features, drivers, mistakes and tricks of Node.js .
Insight, imagination and expertly engineered solutions to accelerate and sustain progress.
Contact