My name is Anton, I'm an expert javascript developer who specializes in React JS/Next JS. Let’s connect: antonfranzen.dev
Published on
If you have built multiple rest APIs with Node JS, you’ll understand the importance of solid error handling, logging, and monitoring.
This tutorial will demonstrate how to handle errors in a Node JS app properly and set up logging & monitoring so you can sleep at night. In the end, we'll have a working Node JS express app with logging and monitoring, so let's get started!
If you implement error handling the wrong way, debugging the app will be a nightmare. If you don't have logging in place, figuring out what went wrong will be even more complicated when the app is in use by many users.
You should aim to implement your error handling in a way that makes debugging easier. For example, you should set up logs that log inbound and outbound requests so you can go back and check what happened when an issue occurred. You should also ensure that the server does not experience any downtime. You can do this by restarting the node process immediately after it has been shut down, as well as running many processors at the same time.
To handle errors correctly and successfully, we must first learn about the node js runtime and the process life cycle.
To begin, Node JS is a javascript runtime that executes and runs javascript in an environment.
We could spend all day discussing the ins and outs of Node JS runtime. Still, the main thing to remember is that it is a runtime in which many different pieces interact and execute our software. Node runs in a single-threaded process, which means it runs code in a single process and has a load limit. So, if we want to avoid downtime and increase the demand on our server, we must run multiple processes.
So let’s talk about the Node JS process and its life cycle.
Have you ever wondered how a node server remains running even when no requests are coming in? This is because the Node JS process that runs the program only runs because of the event loop. The event loop executes events on the call stack and keeps the process running, but if the event loop runs out of things to do, the process will exit. So, for example, when you start your server with the express, the event loop is tricked into thinking it has jobs to accomplish. The process then continues to run as a result. Quite cool, right?
As we now know, the Node JS process is running because the event loop is busy, and the process will terminate when the event loop has run out of tasks to complete.
A Node JS process might also terminate in other ways, such as when an uncaughtException happens, which this article is all about: handling those errors properly.
The goal is to understand what to do when a process terminates and how to log when it happens, so let's go over the two most common error events in Node and what causes them.
To properly implement error handling, you must first understand the various error events and what causes them. In Node JS, there are two primary sorts of errors:
I'm sure you've seen this error before, uncaughtException is something that you, the developer, have messed up. For example, an API call is done without catching the error. It occurs when a javascript error is not correctly handled — when an error happens, and it is not caught. And as we know, the process will shut down if it encounters a uncaughtException.
This error is a promise that rejects and that you haven’t appropriately handled most of the time.
So, for example, it could happen if you have a promise that does a network request and something goes wrong, causing it to reject, and you never catch it. And as we know, the process will shut down if that encounters.
There are ways to avoid the process exiting if any of these errors occur. Still, you should not keep the process running if an uncaughtException occurs by chance. Why? First and foremost, something far worse may occur a short time later. This is because something obviously went wrong when it encountered that problem. Since you have no idea what it may have been, allowing it to continue operating could result in serious damage. Allowing the process to terminate and then rapidly restarting it is what you want to do instead. Doing this ensures that everything begins in a clean state and works as expected.
Errors can occur, and the node process will shut down if we miss them. On the other hand, if we do nothing, the server will experience downtime, which we obviously do not want. Now that we've covered everything to know about the node process, let's look at why we need logging and monitoring and how we can use logging and monitoring to manage those error occurrences.
So after what we went through, it's pretty clear to see why we would need monitoring for our node server that checks if the processes are alive and manage them properly. Moreover, to see the load the node server is getting, the number of requests to certain endpoints, amount of requests per second, CPU usage, memory usage, and a few other things.
There are various tools available for monitoring our node server; however, we will utilize pm2, an open-source monitoring tool for this article. I used pm2 specifically for this purpose in my previous production node program. It supports everything I said, and it has worked flawlessly so far.
So let's say a process shuts down. You need to save the logs to see what happened to go into the code and fix it. So you need to implement a robust error logging system on shutdowns, etc., and save the stack error, where it happened, and other important log data, among other things.
pm2 does offer to log, but I'm not sure it's good enough to allow you to customize and obtain precisely what you want when you want it. As a result, for logging, we'll utilize Winston, an open-source logging tool for Node JS projects.
The problem is that while we can use the native built-in logs such as console.log(), console.warn(), etc., there is one drawback: it only writes to the console, which isn't a reliable way to store logs. So instead, we'd want to save them in a file on the server or in a remote database. And that's where Winston comes in. It lets us choose where we want to save our logs, among other useful things.
Now the fun begins! So what are we going to do?
Well, a few things:
First, let’s create a new project...
Navigate to the new project directory:
Initialize a new project:
Next, let's install express:
Let’s spin up a express server:
This will create a local server at localhost 5000 which we can make get requests to.
Now when we have our express server running, let’s install pm2 and setup monitoring:
Next, let's start our server with pm2, which is literally as easy as this:
This will start our local server at localhost 5000 too, but not its managed by pm2.
And again pm2 makes it so easy for us to set up a badass monitoring with 1 line:
pm2 plus will create a website for us with a lovely server monitoring dashboard, which is extremely cool! This is what it contains:
This is what it looks like now that I've run the command:
Now that we've connected PM2 to our server for monitoring, the following step is to ensure that the server restarts when it encounters an error.
The nice thing about pm2 is that it will automatically restart the process if it exits. So, if you utilize PM2, you have that feature right out of the box.
If you would have a exit.process somewhere in your code, normally the process would terminate as we talked about before. But with PM2 it will shut down and restart directly again, which is exactly what we wanted!
Although it is important to note that even if a process is restarted, it may take some time in certain scenarios, which is why you should definitely run numerous processes at the same time, not only to optimize for zero downtime, but also to spread the burden on your server across different processes.
Now it's time to set up logging for our error events as well as other vital logs!
First, let’s install the logging library winston:
Next let’s create a logging file which we will create a logging module:
We're going to make a logger module that has one type of logger that we can name an info log or an error log, and it will be saved to a file. We now import and use this module whenever we need to log something.
Let's add this to index.js so we can utilize it to make loggs:
Here we log the query on incoming requests and the outgoing response data.
It gets a little trickier if we want to log something when an uncaughtException occurs with express, because by default, all uncaughtException error events are caught by an express middleware. So, when we use express, we can't complete the operation. instead, we add our own middleware as an error handler, and if an uncaughtException error occurs or any other error that isn’t caught elsewhere, it will be logged in the middleware1
To do this, lets add this code to our index.js file:
This middleware will now handle any errors that are not caught.
That’s it. Today, you learned about the Node JS process, how to handle Node JS errors and setup monitoring and logging for our Node server. If you want a more in depth guide how to set up logs with winston, we recommend this tutorial and for logging in general, we recommend this tutorial.
The entire source code for this tutorial is available in this GitHub repository.
Get visual proof, steps to reproduce and technical logs with one click
Continue reading
Try Bird on your next bug - you’ll love it
“Game changer”
Julie, Head of QA
Try Bird later, from your desktop