Friday 16 March 2012

Error handling in nodeJS

We at Vyclone are developing our web server using Node JS and the Express web server framework. As a server technology, We think Node has has many great characteristics, but a well-developed and consistent story on error handling isn't one of them. In this article we'll explore some of the different situations that arise in every day Node examples and then the strategies we're currently adopting to deal with these.

Http client

Here's a small example to play with. We provide a web service that lets people uploads videos. Once we have a video with a latitude and longitude, we look up a place name using a separate web service, store the coordinates and placename in our local database, and then report the placename back to our user. Here's a first go at the necessary code:

[As an aside, notice the way we're chaining callbacks. We're not using libraries like step or async. Our experience of trying these is that they obscure the flow of control without hiding any complexity. Lots of people have noticed that nesting the callback "naturally" just means you run out of space at the right hand side of the editor screen. So, we just chain with named functions that describe the next step in a process. We do use async when we want things to happen in parallel.]

Notice the 'response' object we've passed in - we're in the context of a request to our server, and this is the HttpResponse on which we'll respond to our client. Anyway, the code is somewhat complete, and on one of those days when everything pans out perfectly, it might even work.

However there's absolutely no error handling. Here's a few things that might go wrong:
  1. the http request might encounter an error. These are signalled using an 'error' event.
  2. the json parse might fail. This synchronous call will throw an exception in that case. Alternatively, we might get an exception even after a successful parse if have the structure wrong (that is, there turns out not be a "name" inside a "place" inside a "result").
  3. the database store might fail. Notice our callback from the database uses the common Node practice of passing us a first parameter, "err", whose value is either undefined (nothing went wrong) or an Error object.
  4. something goes unexpectedly wrong in the library. For example, if there's a DNS failure, Node's HTTPClient doesn't tell us using an 'Error' event, it just throws an (unhandled) exception to Node.
And there's the problem with error handling in Node: there are so many different ways that errors can announce themselves.

Our goal is to capture all the different errors types and handle them in the same way, and not scatter too much clutter over our code. To this end, we've written ourselves some general-purpose error handling support. Here's how our example looks with these error handlers applied:

There are three mystery functions here: errorHandlerFunc, tryCatchFunc and handleErrors.

errorHandlerFunc returns a function that takes an Error as an argument, and does something suitable. In this code it's a closure around our response object so that after logging any errors it receives it can send a suitable response to our client. In reality we have a few different error handlers that do different things in different contexts. We use this directly to deal with type 1 errors from our earlier list in the function 'dealWithError'.

tryCatchFunc returns a function tryCatch. This function executes its argument - assumed to be a function - passing on any arguments it receives, but inside a try-catch block. This traps any thrown Errors and passes them to our error handler. This deals with type 2 errors. For example, when we parse json, any errors thrown are eventually caught by the tryCatch wrapped around storeLocation.

handleErrors checks its argument. If the argument evaluates to true, it assumes that the argument is an Error and throws it, otherwise it does nothing. We're careful only to do this when we're inside our tryCatch protection, so that these errors also end up getting passed to our error handler. This deals with type 3 errors: for example, it ensures that any errors we get from our database are thrown to the tryCatch wrapped around sendResponse.

Here's a partial implementation of the generic error handlers:

Hopefully that's all fairly clear.

So, that's wrapped up nearly all our errors and processed them through one route. There are a couple of outstanding problems for another day:

  1. We aren't handling our type 4 errors. These are kind of tricky: node 0.6 lets you catch unhandled errors, but then what? Our approach is to restart the server - it only takes a few seconds, and we're in the fortunate position of operating on a scale where there's a group of similar servers to take the strain in the meantime. Anything else seems unsafe.
  2. The Error objects aren't terribly useful for problem diagnosis in cases 1 and 3. That's because the only part our our code that's mentioned in the stack trace is the generic error handling code - it's not easy to tell where our code went wrong. For this reason, the production versions of our error handling code are more complicated in order to doctor the stacks and to allow coders to add extra messages where it might be worthwhile. More on this another time.

1 comment: