Node.js Error Handling: The Ultimate Guide to Debugging and Handling Errors.

When building backend applications using express (a node.Js framework for building web applications), we usually pass errors via a global error handling middleware. This then sends relevant error messages down to the client depending on the type of error that occurs.

In this article, I am going to take a deep dive into the two errors I encountered in the course of building an application. They are unhandled rejections and uncaught exceptions.

In a simple application, it just might be possible to catch all errors. Still, as the application grows bigger, some errors may be uncaught which may make the server exhibit unexpected and unwanted behaviors.

While building my application, I learned a way of handling these errors. One-word, event listeners (technically, that's two words, but, you get the point).

What is an unhandled promise rejection?

A promise represents an asynchronous operation with three states: pending, fulfilled, and rejected. If an error occurs during the operation, the promise gets rejected. An unhandled rejection means that no catch block or event listener has been configured to handle this error.

Unhandled promise rejection means that somewhere in our code, there was a promise that got rejected, and that rejection was not handled anywhere in our code.

For instance, errors may occur outside of express, eg. the application may fail to connect to the database. (the database may be down or we may have incorrect login credentials). This is an example of unhandled promise rejection. You can use the code sample below to send an error message to the console.

The command process.exit(1) is very abrupt, so we have to properly shut down the server by calling app.close() and then running process.exit() as a callback function to ensure that the server closes properly.

process.on('unhandledRejection', err => {
console.log(err);
app.close(() => {
process.exit();
})
});

Uncaught Exceptions

on the other hand, there are uncaught exceptions and they include all bugs that occur on our synchronous codes. for instance, we are trying to console.log something that does not exist, and right away we get:

"ReferenceError: x is not defined".

An uncaught exception listener is placed before the application is started so that it can start listening right away.

process.on('uncaughtException', (err) => {
    console.log(err);
app.close(() => {
process.exit();
})
});

whenever there is an uncaught exception, the entire process has to be terminated and restarted because it is in an unclean state.

You may ask, what happens after the server is abruptly shut down? There are usually tooling and services available to restart the server as soon as it crashes (think of them as cron jobs). So, that's sorted out.

ideally, errors should be handled right away when they occur, but often, there are exceptions such as the ones mentioned above (uncaught exceptions and unhandled ejections).

unhandled promise rejection and uncaught exceptions may crash your application or make it exhibit unexpected behavior, and may also make debugging more difficult. therefore, they should be used as a last resort. There will always be errors in our code, therefore we as software developers have to ensure that there is a safety net to handle all possible errors.