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.
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
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.
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:
The behaviour can be reproduced by testing the server with ApacheBench (
In order to fix these kinds of errors, add a
.catch() call to the promise and handle the rejection:
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.
To install the module, use
npm install make-promises-safe --save , which will also make it a dependency of the project.
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
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
Promises are number 8 on our list of features in our article about the top 10 features, drivers, mistakes and tricks of Node.js .