Error handling is one of the many things that developers often put aside to do later — only to never quite get around to its proper implementation. While it may seem somewhat trivial, mishandling of errors can lead to application vulnerabilities. A major issue in software that I’ve encountered is the amount of silent fails that are unhandled. The second major issue is applying a blanket bypass to deal with various types of fails.
It doesn’t matter how good a language, framework, or library is — it is ultimately our responsibility as developers to implement error-handling properly. This piece will go over the different types of errors in Node.js, common error handling cases, and why it matters for your application’s security.
Categorizing error types in Node.js
Regardless of language, when it comes to backend API development, there are two types of errors: operational and programmer errors.
With operational errors, failures occur because the runtime doesn’t produce the expected result. When it comes to operational errors, the error itself is not an actual bug — rather, it is something to do with the actual outcome not meeting the minimum requirements. For example, your API gateway connection might have timed out, or the expected input doesn’t match the formatting required.
In contrast, programming errors occur because there is an actual issue with the code itself. This may be due to poorly written logic, unhandled properties, or mishandled loops. Whatever caused the issue involves changing the code to fix it.
It is important to recognize and be able to categorize these issues into their respective groups. Programming errors are caused by developers, which means that the code itself needs to be fixed. In this situation, error-handling becomes a tool that alerts the developer of the problem and creates logs for bug traceability. Operational errors, in contrast, are created by external factors such as failure to connect to a database server, or the system running out of processing memory.
But before we can figure out what kind of error is produced, we will need information. This is where effective error handling comes in.
How to handle errors in Node.js
When building APIs, a good portion of our code deals with callbacks. In the real world, your code will most likely depend on external APIs and the process looks something like this:
- you call the API
- you expect something back in return
A lot of tutorials online advise that we write our callback functions like this:
While the above pattern looks solid with its ability to handle errors, it comes with a logical flaw — you are assuming that the response from res is the correct kind. Your API may have returned an error and a response, and you wouldn’t know because you’ve omitted the possibility of it ever occurring. The security issue here is that if the API you’re using is compromised in some way, you wouldn’t know. This is where your code ‘eats up’ your errors because it doesn’t have the opportunity to be called in every instance despite its existence in the code.
To fix this issue, you need to assume the worst-case scenarios and allow the tests to occur before getting to the processes and procedures. Here is a refactored version of the code above for better error handling in Node.js
Running the check on the callback first will ensure that errors are caught immediately rather than as a last port of thought.
One thing that often occurs in code is that developers ignore errors completely. The error is technically handled — but it isn’t handled at all in real terms because nothing is being done about it. For example, let’s take a look at the code below:
In the above code, not all HTTP statuses are handled. There is no logging involved — just a simple JSON output when a 500 status error occurs. It also doesn’t account for 400 errors. These are caused by failure due to malformed data sent by the user. When no logging takes place, we miss out on vital pieces of information that may lead us to a bigger issue. It makes it harder for developers to debug issues and trace down any potential vulnerabilities or attacks that may be occurring on the server.
To fix this, it is good practice to write a centralized function that logs error output as it occurs. Something should also be monitoring the logs to ensure that these errors are not happening mass frequency. But in the event that they do, developers can be notified of it.
But Promises also come with their own set of issues and potential security sinkholes. Let’s take a look at the following code example:
While this looks innocently simple enough, fs.mkdir is already branching off into another promise chain without actually closing off the second promise — fs.writeFile. One way to handle this is to add a catch to fs.writeFile, but it creates another issue for us. It doesn’t scale well and can quickly put us into a “callback hell” situation.
Here is an example of the beginnings of a callback handling nest.
If we somehow ended up with another promise chain, things can quickly start to visually look messy, therefore making it harder to track and trace where things begin and end. The general rule is to avoid Promises where you can, or use alternatives to the traditional ways of writing Promises.
Why proper error handling matters for your application’s security
Logging can be a gold mine for detecting issues before they become major burns in your software’s integrity. Oftentimes, developers do not think about error handling as a security exercise but something that can be ignored and dismissed unless it directly impacts the application in some way.
The issue with this thinking is that it forces errors to fail silently — meaning that when things do go wrong, we are playing Russian roulette with a potentially major failure. A malicious user can be repeatedly trying out different methods to cause failures in our application until something finally slips through — and we would not know anything about it.
This is why having information is vital and it starts with properly logging out errors and creating monitoring systems to ensure that the errors are not excessive in occurrence. Having logs can also help distinguish between operational and programmer errors. When operational errors occur too many times, there might be an issue on your infrastructure that needs to be explored. If a programmer error occurs, then it is a bug that needs to be fixed.
Whatever the cases are, when errors are properly handled, they give the developer metadata to help solve short and long-term issues. Without it, we are mostly debugging blindly.