⬆️ ⬇️

Handling asynchronous errors with saving the request context in connect / express

Those who had to develop more or less large web-projects for node.js, probably faced with the problem of handling errors that occurred within asynchronous calls. This problem usually pops up not immediately, but 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 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 call occurs synchronously and error handling is no different from a similar task in other languages.

')

Now we will 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, since it does not occur during a function call, but inside an asynchronous call that occurs 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 the error in the first argument of the callback function.



This approach completely solves the problem of handling errors within asynchronous calls, but it greatly inflates the code when there are many such calls. In a real application, there are many methods that call each other, can have nested calls and be part of chains of asynchronous calls. And every time an error occurs somewhere in the depth of the call stack, we need to “deliver” it to the very top, where we can properly handle it and inform the user about the abnormal situation. In a synchronous application, try / catch does it 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 transfer it up the stack of calls.



Decision





In Node.JS, starting from version 0.8.0, a mechanism called Domain appeared . It allows you to catch errors inside asynchronous calls, while maintaining the execution context, unlike process.on ('uncaughtException'). I think there is no sense in retelling the Domain documentation here, because the mechanism of its operation is quite simple, so I will immediately move on to the specific implementation of the universal error handler for connect / express.



Connect / express wraps all middleware into try / catch blocks, so if you do a throw inside the middleware, the error will be passed to the error handler chain (middleware with 4 input arguments), and if there is 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 occurred in the synchronous code.



With Domain, we can redirect errors that occur within asynchronous calls, in the context of a request, into the chain of error handlers 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, which solves this problem. The module is available on GitHub and in npm .



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 treated the same. You can throw an error at any depth of calls 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 this article remains experimental, but I already use the described approach, even though I have not seen any problems in a small production. The site using this module has never crashed and does not suffer from memory leaks. Uptime process more than a month.

Source: https://habr.com/ru/post/161889/



All Articles