Promises, errors and Express.js

07 Jul 2013

After deciding to give promises a try for a new Node app, we realised 2 things: promises rock for legibility. Promises can also be hell to troubleshoot… if you don't follow a few simple rules.

Our main problem was error handling in an Express.js app. Some routes would crash the server or even just hang there… Of course the best way to figure it all out was to write a lot of unit tests. I'll give a summary below but check out promises-and-errors on Github for all the detail!

The usual callback style

Let's have a quick look at the usual Node-style callbacks:

Expected errors are usally handled as function paramters::

doSomething(function(err, val) {
    if (err) console.log('oh no!');
    else console.log(val);
});

On the other hand, unexpected errors just bubble up to the Node.js error handler and crash your entire process (including an Express.js server).

somethingElse(function() {
    // if this was a "real" error it should be handled
    // but say we just mistyped something temporarily
    does.not.exist();
});

Note that the error message (ReferenceError) will show in the console, which is good for troubleshooting.

Error handling in promises

Now we can compare that to 2 big promise libraries:

Both libraries actually have the same behaviour, but differ in their naming conventions (see the unit tests on Github for the differences). Test 3, 4 and 5 are interesting: in the case of unexpected errors, promises gracefully call your failure handler if you provide one:

myPromise.then(function(val) {
    does.not.exist();
}).fail(function() {
    res.send(500, 'something went wrong');
});

However they will just hang if you're feeling optimistic:

myPromise.then(function(val) {
    does.not.exist();
    res.send('success!');
});

In an Express app, this means the browser will never receive a response (e.g. ERR_EMPTY_RESPONSE in Chrome). The error doesn't even show in the console, so that's great for debugging! However if you let the library know that you're done chaining promises, the error is thrown properly and we get the same behaviour as callbacks: it terminates the Node process. Not great, but at least consistent!

myPromise.then(function(val) {
    does.not.exist();
}).done();

So always provide a failure handler, or terminate your promise chain!

What was that about domains?

Domains are a relatively new (still unstable) feature of Node.js that lets you group operations and capture all errors in that context. In the case of Express, we can wrap every request in its own domain, and it looks something like this. Now be careful, this is still experiemental and likely to change in future versions of Node.

app.use(function(req, res, next) {
    var requestDomain = domain.create();
    requestDomain.add(req);
    requestDomain.add(res);
    requestDomain.on('error', next);
    requestDomain.run(next);
});

Uncaught errors are now forwarded to Express:

Standard error message

We can then use the default error handler to get a much nicer looking error page & stack trace:

app.use(express.errorHandler({dumpExceptions: true, showStack: true}));

Pretty stack trace

Finally: the case of Mongoose

Although Mongoose uses mpromise internally, it also does some interesting things on top. This means the advice about ending promises is still valid (mpromise calls them onReject and end), but the domain code won't work. The following example doesn't trigger the Express error handler, and just crashes the server:

Thing.findById(1).exec().then(function(val) {
    does.not.exist();
}).end();

If someone can shed some light, I'd love to understand exactly why. In the meantime, an easy solution is to wrap Mongoose calls into a Q promise:

var p = Q.when Thing.findById(1).exec();
p.then(function(val) {
    does.not.exist();
}).done();

And we're back to normal!

Comments