As with any software, here you can expect the unexpected. Node apps experience errors as well. Let’s say that an error crops in our Node API – what should we do about it?
Errors can be handled in a lot of different ways. “Handling” in this article will essentially mean error response messaging.
Specific Messages are Best
When handling errors, we should do so as specifically and quickly as possible. As an example, if we can respond to a request that causes an error with a more specific error message, this would be better than responding with a generic, catch-all message. This first message:
is better than this:
But some error messaging is better than nothing.
So, starting with the most specific handling cases and going to the most generic.
Handle Specific Business Errors
Let’s say that we have an endpoint that creates a new book in our books database, requested at
POST /books. It validates the request body data shape to ensure sufficient and correct data is entered for new books. Let’s say that a user submits a new book without the required title field. It would be great to have specific response that tells the client what exactly is wrong. The controller code, in part, might look like this in Express:
1 2 3 4 5 6 7 8 9 10
That last line is the error response with specific error messages. These messages are presumeably generated by the
validate function. We can provide a specific
bad request response status code with a
400. We can provide the specific error response body, which would look like:
Catch All Other Errors*
Now there are other errors that might occur that we either haven’t, don’t want, or will never be able to anticipate enough to provide specific handling for. In these cases, we still want to handle the error, but we’ll only be able to provide minimal value and insight into the nature of the error to clients in the responses.
Let’s adjust our controller code to handle potentially errors that might happen in the book saving process. What could happen? Anything… like database issues with connections, constraints, locks or just something bad in our code. In our callback, let’s do something with that potential error:
1 2 3 4 5 6 7 8 9 10 11 12
Required Error Contract
err that comes back should be an
Error or a subtype that at least follows the error contract and has the 3 required fields of
name to identify the type of error,
message for the human readable main issue of the error, and
stack a string of the accurate location and stack trace of the error.
next for Errors
next function in Express will advance to the next middleware. In the case of passing a non-null value to
next, remaining normal middleware will be skipped and the first error middleware will be executed. Error middleware functions have an arity of 4 parameters instead of the usual 3, where
err is the first parameter. Here’s a basic catch-all error handler:
1 2 3
serializeError is just going to take that and transform it into something to push across the network in the response.
1 2 3
Don’t Leak Stacktraces
Let’s add a little something else here. We probably don’t want to leak the stack trace to our users in production, so let’s protect it by detecting
NODE_ENV and update it to be something like:
1 2 3 4 5 6
Even better. Generic errors handled.
Crash On the Rest
Now note the asterisk on the previous section. We aren’t actually able to catch all errors in that generic “catch-all” error handler. To illustrate, let’s create two new routes. The first is a route that does nothing but throw a new
1 2 3
The above route, if requested at
GET /debug/error, would throw a new Error and it would be caught by the generic error handler of the previous section. This is because
throw delivers that new
Error synchronously. It stays in the context of the current call stack of the request through Express middleware. And Express can catch the
Error and call your first error-handling middleware.
But we can easily break out of this context and bypass the catch-all handler entirely. All we have to do is use a
setTimeout will queue a new message for the event loop to, at some later tick of the clock, run the enclosed function. Within that function, we’ll throw another
1 2 3 4 5
Let the Process Die
And now there’s really nothing that Express can do to help us. The
Error will instead bubble up to the Node process running our app. That process gives us one final opportunity to know about the occurrence of such a fatal error. Once we know about it, we can log it. Perhaps we can get off a call to our monitoring service. After that, we should assume our process is compromised, potentially unstable, and just crash. Then use a tool like forever to detect the downed process and restart it. Such a crash handler might look like:
1 2 3 4 5
The call to
process.nextTick is meant to give some leeway for just enough processing time to finish those last ditch logging/monitoring efforts.
Now we have caught all the errors, in hopefully the best and most helpful way possible.
What other things have you done in your app to make Node error handling better?