How to prevent your Node.js process from crashing

Interviewing some people for Node.js job positions I found two questions that nobody was answering correctly, and that frightened me:

  • What happens if you don’t handle a rejected promise in Node.js? How can you debug that?
  • What happens if you don’t handle the ‘error’ event in a stream?

Think about it. Do you know the answers? Are you sure? If not, you can compare yourself with this guy:

What happens if you don’t handle a rejected promise in Node.js? How can you debug that?

In the latest versions of Node.js (8.x, 9.x and probably earlier versions too) you get a few warnings printed to the stderr like this:

(node:48446) UnhandledPromiseRejectionWarning: This is fine
(node:48446) 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:48446) [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.

There are two important things here:

  • The process doesn’t crash. The process continues to work. But you get noticed.
  • However, this behavior will change in the future! In future versions of Node.js the process will crash.

This is great to know. Now you might be wondering how you can react to unhandled rejections programmatically. For example you might be interested in sending the stack trace to a crash reporting service. No worries, Node.js has something for you. You can listen to the unhandledRejection event emitted by process

process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', reason.stack || reason)
// Recommended: send the information to sentry.io
// or whatever crash reporting service you use
})

For promises rejected with an error (e.g. Promise.reject(new Error('This is fine'))) this will print both the message and the stack trace. In any other case this will print the object passed to reject the promise. So, it is important to always reject with an error object in order to get a stack trace!

You can also listen to the warning event emitted by process and you will get information about unhandled rejections and much more.

What happens if you don’t handle the ‘error’ event in a stream?

Create an app with just the following code:

const fs = require('fs')
const stream = fs.createReadStream('does-not-exist.txt')

What happens? BOOM! 💥 Your process exits with a non-zero exit code and this is printed:

events.js:137
throw er; // Unhandled 'error' event
^
Error: ENOENT: no such file or directory, open 'do-not-exists.txt'

Now, imagine somebody is uploading a file to your website, you are handling it with streams because it can be a big file, the user forgets about it and closes his laptop. The stream closes unexpectedly and… your Node.js process crashes and all the users of your app get errors. You don’t want that, right? Then, do not forget to handle the error event in all your streams!

Extra ball: JSON.parse()

This apparently harmless function will throw an error if the input string is not valid JSON. Always, always, always add a try-catch function for it or wrap it in a promise. The same applies to any *Sync() method in the Node.js API. Otherwise something like this will kill your process:

app.get('/foo', (req, res, next) => {
const foo = JSON.parse(req.body.jsonString)
// ...
})

Simple and nice tic-tac bomb ☝️ waiting to crash your app on a Sunday.

Remember:

  • Always handle promise rejections. Listen to unhandled rejection events for crash reporting. And always use errors to reject promises in order to get stack traces!
  • Always handle the error event for streams!
  • Wrap JSON.parse() and any*Sync() function in a try-catch block or inside a Promise. Well, in general any function that can throw errors.