How to Build an Effective Error-Handling System in Node.js
- Dec 8, 2020
- 6 min read
Updated: Dec 9, 2020
Handling errors in Node.js is not only useful but also necessary in large-scale applications. Without the utilisation of the right techniques of error handling, your code might be unclean, and it might be very challenging to find, and handle errors in your app.

There is a couple of reasons why you need a reliable error-handling system in your application.
First of all, you want to be sure that you reduce your development time by finding and fixing your bugs quickly. Secondly, you want to differentiate between different types of errors. It is then beneficial for both you and your users because they won't get some strange-looking errors they can't understand.
If you're interested in building such a system, then this guide is for you.
Differentiation of Errors in Node.js
First of all, let's talk about two types of errors that occur in Node.js. Fundamentally, two types of them can happen in your app - operational errors and programmer errors.
Operational errors are errors that have nothing to do with bugs in your code. They are inevitable in any application and deal with users and server rather than code itself. The examples of such errors are failures to connect with the server, validator errors, invalid path accessed etc.
Programming errors, on the other hand, usually represent bugs in your code and various mistakes that could be the fault of a developer. The examples could be trying to read the properties on undefined, using await without async etc.
So, to put it simply, operational errors happen naturally in every application and programmer errors are usually bugs caused by developers.
Now comes the critical question: Why do you even need to differentiate them? First of all, operational errors aren't usually that dangerous to your app. You don't want to close your application because someone's accessed the wrong path in your API, do you?
Programming errors, on the other hand - they are more dangerous. When you deal with them, you might want to close the application because they might cause severe problems and create snowball effects.
That's why you need to know whether an error is a potential threat to your application - will it cause unwanted behaviour? Maybe there's an issue with its security? You need to know what you're dealing with so that you can handle it properly.
Creating an Effective System to Handle Errors
Now comes the exciting part. There are many ways in which you can approach creating the error-handling system. However, developers often create a global middleware that handles all types of errors that are coming from an app.
I'm going to show you how this works on a simple example. Let's say you want to secure all the unspecified routes in your API so that when someone tries to send a request - he gets an error that says it is undefined.
Express comes with a default error handler so you won't need to write your own in this particular example.
Firstly, let's focus on creating a middleware that handles the error.
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500; // The standard status code for unspecified errors
err.status = err.status || ‘error’; // The standard status
res.status(err.statusCode).json({ // Creating the response
status: err.status,
message: err.message,
});
});Now let's analyse this code line by line. As you can see, this middleware is built the same way as any other middleware in Express. The only difference is that to the error-handling middleware, you pass four arguments instead of three.
Once you create the function, you specify the default status code in case that it's not coming from the error you determined. As you can see the standard code for such situations is 500, which means an "internal server error".
You do the same thing with status, and you create a response.
Once you create the middleware, you can then secure the undefined routes in the following way.
app.all(‘*’, (req, res, next) => {
const err = new Error(`Sorry, I couldn't find ${req.originalUrl} on this server!`);
err.status = ‘fail’;
err.statusCode = 404;
next(err);
}Now, this works, and it's surprisingly simple to set up. But what if I told you that you could improve the error handling even more? The real magic happens when you create your own error class. Let's see how to do it.
class AppError extends Error {
constuctor(message, statusCode) {
super(message); // Calling the parent constructor
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith(‘4’) ? ‘fail’ : ‘error’;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}Now assuming you have some javascript knowledge, the class itself shouldn't be too difficult to understand. We define the same variables as in our previous example with the middleware.
What's different here is the "isOperational" variable and "captureStackTrace" method. The isOperational variable lets us know - the developers that the error is operational in this instance. Hence, the handling of this error will look different than in the case of a programming error. Later you can always check for this property, and send the users adequate messages.
The "captureStackTrace" method will send you the essential information about an error itself. That will help you keep track of the root of an error.
Once you create the class, you can then see how easy it is to handle the same error as we did previously.
app.all(‘*’, (req, res, next) => {
next(new AppError(`Can’t find ${req.originalUrl} on this server!`, 404));
}There are many other practices that you could implement to even more improve the error handling system we've created so far. For example, you could throw different errors during production and development.
As you can probably imagine, you wouldn't want to leak too much information during production. Whereas during development, the errors should be as specific as they can get to simplify resolving them.
Remember that every error-handling system might be slightly different depending on the type of your application. A lot of work associated with handling such errors is based around predicting what types of them can occur during both production and development and then handling them accordingly.
You can do all of that in the middleware we've previously created. The types of errors that you might take into consideration are the cast errors, validation errors, duplicate fields etc.
Lastly, I recommend downloading some npm package to enhance the system a bit more. There are two npm packages that are worth checking out which are morgan and winston. They basically provide loggings of different types, colours, formatting and different outputs depending on the runtime environment.
I prefer to use morgan just because I'm more familiar with it, but I highly recommend checking out both of them 😄!
Unhandled Rejections & Uncaught Exceptions
While what we've done so far is effective whenever an error occurs in the application, it won't consider all the errors occurring outside of it. The simple example of that might be failing to connect to the database.
Since it'd happen outside of the Express application, our error handler wouldn't catch it. That's why we need to consider those scenarios as well.
Unhandled promise rejections mean that somewhere in your code, there is a promise that was rejected and unhandled properly. Without handling them accurately, your application might be suddenly closed in the least pleasant moment.
So how to handle them properly?
Let me show you:
const port = 8000;
const server = app.listen(port, () => {
console.log(`App running on port ${port}`)
})
process.on('unhandledRejection', err => {
console.log(err.name, err.message);
server.close(() => {
process.exit(1);
});
});As you can see, we use the global object named "process", and we use it to manage the unhandled rejection. Once we catch the unhandled promise, we have to make sure we close the server first before shutting down the application. Otherwise, every incoming request would be suddenly cancelled, which could have devastating effects on the real-world app.
Okay, you've learned how to manage unhandled promise rejections, now the last thing to do is to handle uncaught exceptions in our synchronous code.
As you can already be guessing, uncaught exceptions are all the bugs that occur in our synchronous code. Luckily, handling those types of errors is very similar to unhandled rejections. Here's how to manage them
process.on('uncaughtException', err => {
console.log(err);
console.log('Uncaught exception, shutting down!')
server.close(() => {
process.exit(1);
});
});Conclusion
So, I'm wrapping up this article with a deep hope that it was useful to you. Hopefully, you have a better understanding of how to handle errors in Node.js. Of course, there is still a lot of information and techniques to learn, so if you're interested in the future articles, subscribe to my blog to keep yourself updated 😄.




Comments