Handling asynchronous errors while maintaining the request context in connect / express

    Those who had to develop more or less large web projects on node.js, probably faced with the problem of handling errors that occurred inside asynchronous calls. This problem usually does not come up right away, and when you already have a lot of written code that does something more than "Hello, World!"

    The essence of the problem


    For example, take a simple connect application:

    var connect = require('connect');
    var getName = function () {
    	if (Math.random() > 0.5) {
    		throw new Error('Can\'t get name');
    	} else {
    		return 'World';
    	}
    };
    var app = connect()
    	.use(function (req, res, next) {
    		try {
    			var name = getName();
    			res.end('Hello, ' + name + '!');
    		} catch (e) {
    			next(e);
    		}
    	})
    	.use(function (err, req, res, next) {
    		res.end('Error: ' + err.message);
    	});
    app.listen(3000);
    

    Here we have a synchronous function that, with some degree of probability, generates an error. We catch this error and pass it to the general error handler, which, in turn, shows the error to the user. In this example, the function is called synchronously and error handling is no different from a similar task in other languages.

    Now let's try to do the same, but the getName function will be asynchronous:

    var connect = require('connect');
    var getName = function (callback) {
    	process.nextTick(function () {
    		if (Math.random() > 0.5) {
    			callback(new Error('Can\'t get name'));
    		} else {
    			callback(null, 'World');
    		}
    	});
    };
    var app = connect()
    	.use(function (req, res, next) {
    		getName(function(err, name) {
    			if (err) return next(err);
    			res.end('Hello, ' + name + '!');
    		});
    	})
    	.use(function (err, req, res, next) {
    		res.end('Error: ' + err.message);
    	});
    app.listen(3000);
    

    In this example, we can no longer catch the error through try / catch, because it does not occur during a function call, but inside an asynchronous call that will happen later (in this example, at the next iteration of the event loop). Therefore, we used the approach recommended by the developers of node.js - we pass an error in the first argument to the callback function.

    This approach completely solves the problem of error handling inside asynchronous calls, but it greatly inflates the code when there are a lot of such calls. In a real application, many methods appear that call each other, can have nested calls and be part of chains of asynchronous calls. And every time an error occurs somewhere deep in the call stack, we need to “deliver” it to the very top, where we can correctly process it and inform the user about an emergency situation. In a synchronous application, try / catch does this for us - there we can throw an error inside several nested calls and catch it where we can handle it correctly, without having to manually pass it up the call stack.

    Decision



    In Node.JS since version 0.8.0 has a facility called the Domain . It allows you to catch errors inside asynchronous calls, while preserving the execution context, unlike process.on ('uncaughtException'). I think it makes no sense to retell the documentation on Domain here. The mechanism of its operation is quite simple, so I will immediately go on to a specific implementation of the universal error handler for connect / express.

    Connect / express wraps all middleware in try / catch blocks, so if you throw inside middleware, the error will be passed to the chain of error handlers (middleware with 4 input arguments), and if there are no such middleware, to the default error handler which will output trace errors to the browser and console. But this behavior is relevant only for errors that occur in synchronous code.

    Using Domain, we can redirect errors that occur inside asynchronous calls in the context of the request to the error handler chain of this request. Now for us, ultimately, the processing of synchronous and asynchronous errors will look the same.

    For this purpose, I wrote a small middleware module for connect / express that solves this problem. The module is available on GitHub and innpm .

    Usage example:

    var
        connect = require('connect'),
        connectDomain = require('connect-domain');
    var app = connect()
        .use(connectDomain())
        .use(function(req, res){
            if (Math.random() > 0.5) {
                throw new Error('Simple error');
            }
            setTimeout(function() {
                if (Math.random() > 0.5) {
                    throw new Error('Asynchronous error from timeout');
                } else {
                    res.end('Hello from Connect!');
                }
            }, 1000);
        })
        .use(function(err, req, res, next) {
            res.end(err.message);
        });
    app.listen(3000);
    

    In this example, errors thrown inside a synchronous and asynchronous call will be handled the same way. You can throw an error at any call depth in the context of the request, and it will be processed by a chain of error handlers for this request.

    var
        connect = require('connect'),
        connectDomain = require('connect-domain');
    var app = connect()
        .use(connectDomain())
        .use(function(req, res){
            if (Math.random() > 0.5) {
                throw new Error('Simple error');
            }
            setTimeout(function() {
                if (Math.random() > 0.5) {
                    process.nextTick(function() {
                        throw new Error('Asynchronous error from process.nextTick');
                    });
                } else {
                    res.end('Hello from Connect!');
                }
            }, 1000);
        })
        .use(function(err, req, res, next) {
            res.end(err.message);
        });
    app.listen(3000);
    


    In conclusion, I note that officially the stability of the Domain module at the time of writing is still experimental, but I am already using the described approach, even in a small production but I don’t see any problems. A site using this module has never crashed and does not suffer from memory leaks. Uptime process for more than a month.

    Also popular now: