📜 ⬆️ ⬇️

Centralized exception handling in Node.JS



Preamble from the translator: a couple of months ago I was looking for a solution to be able to use exceptions in the game server, written in node.js. Unfortunately, pure exceptions are not quite compatible with the environment running on the event loop. The easiest way to explain this is by example:
try { process.nextTick(function() { throw new Error('Catch Me If You Can'); }); } catch (e) { console.log('Exception caught:', e); } 

This exception, of course, will not be caught, and it will drop the whole process. A month ago, node.js version 0.8.0 saw the light with the fresh (experimental) domain module, which is designed to solve such problems. However, I would like to pay tribute to the class that I still use. Go:

Functional programming in node.js is fun, expressive and compact. Except for one thing - exception handling. This is not often said, but in my opinion, the lack of a harmonious way of handling errors and exceptions is one of the biggest drawbacks of node.js. Node-fibers uses a fully imperative programming style to achieve this, but I would like to solve a problem within a functional style.

The problem with error codes (on which the node.js core is based) is that the code that encounters the error first is almost always not the place to decide how to react to it. In this case, try / catch structures in multi-threaded systems are much more understandable. Someone higher in stack usually knows how to handle the error.
')
However, the problem with asynchronous systems, such as node, is that every time one of your callbacks or EventEmitters is called, it is either called at the very top of the event loop, or it is caused by code other than this listener (the place where we assign the listener is perhaps a better place to handle a potential error than some arbitrary code that caused it). If you throw an exception in such conditions, your program will most likely fall entirely. Considering that there are so many possibilities in JavaScript, when the right code can throw out runtime errors, this problem becomes even worse than in C, where I can avoid problems if I’m careful with pointers and won’t divide by zero. Yes, unit tests help, but it’s more like trying to plug all holes in the sieve when you really just need a bowl.

To achieve this, we need the ability to set a Block with our own Error Handler, which can be created quickly and easily, and passed along with callbacks to an external code. Then, if an exception occurs in the callback, it should be sent to the Block Error Handler that was active when we created the callback. I found that most of these solutions are introduced by Futures, Promises, Fibers, etc. along with this simple functionality for creating Blocks. The following snippet describes the Block class, which does exactly what I need:
 /** *  Block        . */ function Block(errback) { this._parent=Block.current; this._errback=errback; } Block.current=null; /** *  ,   ,     , *           guard(). *     ,   . * * Example: stream.on('end', Block.guard(function() { ... })); */ Block.guard=function(f) { if (this.current) return this.current.guard(f); else return f; }; /** *      .  -     (" try"). *  -    ('catch'). */ Block.begin=function(block, rescue) { var ec=new Block(rescue); return ec.trap(block); }; /** *   function(err),        , *       (  ,    ). *      err  true. * * Example: request.on('error', Block.errorHandler()) */ Block.errorHandler=function() { // Capture the now current Block for later var current=this.current; return function(err) { if (!err) return; if (current) return current.raise(err); else throw err; }; }; /** *    .     error handler,     . *   raise(...)     .    , *        throw. *  ,   error handler'  , *     error handler'. */ Block.prototype.raise=function(err) { if (this._errback) { try { this._errback(err); } catch (nestedE) { if (this._parent) this._parent.raise(nestedE); else throw nestedE; } } else { if (this._parent) this._parent.raise(err); else throw(err); } }; /** *   callback    . *       raise()  . *  return value   undefined   . */ Block.prototype.trap=function(callback) { var origCurrent=Block.current; Block.current=this; try { var ret=callback(); Block.current=origCurrent; return ret; } catch (e) { Block.current=origCurrent; this.raise(e); } }; /** *  ,        . *     trap(),       . */ Block.prototype.guard=function(f) { if (f.__guarded__) return f; var self=this; var wrapped=function() { var origCurrent=Block.current; Block.current=self; try { var ret=f.apply(this, arguments); Block.current=origCurrent; return ret; } catch (e) { Block.current=origCurrent; self.raise(e); } }; wrapped.__guarded__=true; return wrapped; }; 


(I chose the terminology of Block / Rescue, not because I feel affection for Ruby, but because such a solution does not use the words reserved in JS).

Note translator: an example from the beginning of the article, but using Blocks, takes the following form:
 Block.begin(function() { process.nextTick(Block.guard(function() { throw new Error; })); }, function(err) { console.log('Exception caught:', err); }); 
Now the exception is processed, and we can not drop the server. This also works with setTimeout, EventEmitter, callbacks for requests to the database, and anything else.

Now consider an example of using the Block for centralized error handling. In our example, connect 'ovsky middleware is used, in which case the next function is an excellent error handler: it will give the correct error to the http client. If we had to somehow handle the error ourselves, we could simply describe the callback in the form function(err) { ; next(err); } function(err) { ; next(err); } function(err) { ; next(err); } . You can also use inline functions in Block.begin calls for greater visual similarity to try / catch, but I prefer to use named callbacks to increase readability.

 function handleUserAgent(req, res, next) { return Block.begin(process, next); function process() { jsonifyRequest(req, withRequest); //  ,   jsonifyRequest , //    Block.guard() } function withRequest(requestObj) { var r=validators.UserAgentRecord(requestObj, {fix:true}); if (!r.valid) { res.writeHead(400); return res.end('Invalid request object: ' + r.reason); } var uar=r.object; if (uar.token) { // Verify //return handler.verifyUserAgent(uar); throw new Error('verifyUserAgent not yet implemented'); } else { // Create uar.token=null; uar.type='auth'; // TODO: Maybe support unauth in the future? handler.createUserAgent(uar, Block.guard(withUserAgent)); } } function withUserAgent(userAgent) { var r=validators.UserAgentRecord(userAgent, {fix:true}); return respondJson(r.object, res); } } 


The main point to keep in mind is that any exception thrown by this code or the process () function will be forwarded to the error handler (in this case, to the next function). In order to bind the callbacks to the block, they must be wrapped using Block.guard (originalFunction). This will remember the active block at the time of calling Block.guard (), and then restore it as a context before calling the originalFunction () function itself.

Consider another example of the explicit use of a block in callbacks. In this case, we make an HTTP request, accumulate the response text, call the callback, passing the created CouchResponse (which parses the response and other things that can throw an exception) into it.

 request: function(options, callback) { var req=http.request(options, function(res) { var text=''; res.setEncoding('utf8'); res.on('data', function(chunk) { text+=chunk; }); res.on('end', Block.guard(function() { callback(new CouchResponse(res, text)); })); res.on('error', Block.errorHandler()); }); req.on('error', Block.errorHandler()); req.end(); } 


There are still several places where we might encounter unexpected exceptions dropping the whole process:

I could also wrap them using Block.guard, but I think this is redundant. In addition, I am 100% sure that the error in this case is critical, and should be covered by unit tests. The 'end' handler, however, does something that I cannot see immediately (and I know that it contains a JSON.parse call), so I prefer to protect it with guard. Finally, I use the standard errorHandler () block to catch request and response error events. This simple template for centralized error handling makes it pretty clear where these errors go and handle them at any level where it makes sense. You can use nested calls to Block.begin () (analogous to try{try{}catch{}}catch{} ). This is useful in code for frameworks that should do some work within a block created by someone else's code.

PS: The author has an implementation of Future and examples with their use. I did not translate everything related to Future, and adapted the examples for the use of classical callbacks.

I recommend to read the original text in its entirety, because there are as many as 10 recommendations for writing bulletproof code on node.js.

I designed the Block class as a github repository and an npm module (npm install control-block).

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


All Articles