Exception handling in node.js

We don’t see exceptions coming. Whether they’re syntax errors or deliberate assertions about invariants that we really, truly expect to hold, they only arise when we travel beyond the boundaries we defined for our program. We’re off the edge of the map. We never anticipated that we’d be here, but now we need to clean up. And, just as with anticipated async errors, the problem of gracefully handling exceptions is compounded by event-driven code.

For a concrete example, we might anticipate an exception when trying to call a function that doesn’t exist. So we wrap the offending code in a try...catch block to keep the process alive when things go south:

try {
  callback();
}
catch (err) {
  console.error('catastrophe', err);
}

If we try to repeat this in event-driven land, however, we see a surprising result.

// errors/continuation_exception.js
function somethingAsync (callback) {
  setTimeout(function () {
    // Will fail if `callback` is not a function
    callback();
  }, 1);
}

// Don't do this
try {
  somethingAsync();
}
catch (err) {
  console.error('catastrophe', err);
}

Running the example, our handler is skipped entirely:

$ node errors/continuation_exception.js
errors/continuation_exception.js:5
    callback();
    ^
TypeError: undefined is not a function
    at null._onTimeout
(errors/continuation_exception.js:5:5)
    at Timer.listOnTimeout [as ontimeout] (timers.js:112:15)

Notice that the catch was never actually invoked? Because the event callback takes place off the original call stack, the try..catch isn’t around when the stack unwinds. The error reaches the top of the stack, and–boom–down goes the app.

Domains

In version 0.8, Node added the concept of a domain, a sort of scoped catch designed for handling exceptions inside evented code. The idea is fairly simple: once a domain is created, any exceptions beginning in code attached to the domain will be scooped up and forwarded to the domain’s error event.

// errors/domain_exception.js
var domain = require('domain');

function onDomainError (err) {
  console.error('HANDLED', err.name, err.message);
  process.exit(1);
}

function somethingAsync (callback) {

  var d = domain.create().on('error', onDomainError);

  var timer = setTimeout(function () {
    callback();
  }, 1);

  d.add(timer);
}

somethingAsync(null);

In this example, we create a domain (d) and register an error handler to catch any exceptions originating inside it. We then add an event-emitting timer. When the timer fails with an undefined callback, the onDomainError handler will have an opportunity to handle errors in a somewhat controlled manner.

$ node errors/domain_exception.js
HANDLED TypeError undefined is not a function TypeError: undefined is not a
function
    at null._onTimeout
(errors/domain_exception.js:11:5)
    at Timer.listOnTimeout [as ontimeout] (timers.js:112:15)

We now have exceptions channeled into a predictable callback. Great! But domains are every bit as dangerous as they are useful. Per the documentation:

By the very nature of how throw works in JavaScript, there is almost never any way to safely “pick up where you left off”, without leaking references, or creating some other sort of undefined brittle state.

The safest way to respond to a thrown error is to shut down the process.

Local exception-handling requires plenty of boilerplate, but it’s a very deliberate design decision. If we know an exception started with malformed JSON passed to JSON.parse, we reject the input, inform the user and get on with it. By catch-ing errors close to the operation that caused them, we stand a reasonable chance of recognizing the unserved requests, file descriptors, or other resources the exception might have left hanging and ensuring they get cleaned up appropriately.

Now that domains have us operating at a broader scope than our original try..catch, it becomes critically important to make sure that the process goes down when an exception is caught.

The last resort

For errors that aren’t attached to a domain, the node process exposes an 'uncaughtException' event. Here’s the idea: if everything else blows up and an exception reaches the top of the event loop, the event gives us one last chance to make sure the lights are off on the way out.

Node’s familiar default is to dump a stack trace and exit.

$ node -e 'throw new Error()'

[eval]:1
throw new Error()
      ^
Error
    at [eval]:1:7
    at Object.<anonymous> ([eval]-wrapper:6:22)
    at Module._compile (module.js:456:26)
    at evalScript (node.js:536:25)
    at startup (node.js:80:7)
    at node.js:906:3

If the event is hooked in user-land, though, this won’t take place. It’s entirely up to the developer to print traces; dump heap, knock the app out of etcd; drop the mike, unplug–whatever needs to happen as the process goes down. In a simple example, we provide a handler and throw an error:

// uncaught_exception.js
process.on('uncaughtException', function (err) {
  console.error('CAUGHT', err);
  process.exit(1);
});

throw new Error('Something terrible has happened');

This behaves exactly as we would expect:

$ node uncaught_exception.js
CAUGHT [Error: Something terrible has happened]

In conclusion

Exception handling is a critical tool in helping identify and assess sources of production errors, and it can operate at several levels. We’ll handle as much as we can locally; we may also wrap certain pieces of application logic within an explicit domain. If we need app-level handling, we can catch exceptions globally to serve appropriate notice as the process goes down.

No-one likes an exception, but–handled appropriately–every instance is a learning opportunity: a chance to make sure the same issue doesn’t arise again.

Let’s keep in touch

Get noise-free updates on software, product, and process.

Hey, I'm RJ: digital entomologist and intermittent micropoet, writing from the beautiful Rose City.