The instanceof
keyword should be used to check for the specific error type, as shown above. Don’t use the name of the error to check for the type, as in err.name === 'ValidationError'
, because it won’t work if the error is derived from a subclass of ValidationError
.
Types of errors
It is beneficial to distinguish between the different types of errors that can occur in a Node.js application. Generally, errors can be siloed into two main categories: programmer mistakes and operational problems. Bad or incorrect arguments to a function is an example of the first kind of problem, while transient failures when dealing with external APIs are firmly in the second category.
1. Operational errors
Operational errors are mostly expected errors that can occur in the course of application execution. They are not necessarily bugs but are external circumstances that can disrupt the flow of program execution. In such cases, the full impact of the error can be understood and handled appropriately. Some examples of operational errors in Node.js include the following:
An API request fails for some reason (e.g., the server is down or the rate limit exceeded).
A database connection is lost, perhaps due to a faulty network connection.
The OS cannot fulfill your request to open a file or write to it.
The user sends invalid input to the server, such as an invalid phone number or email address.
These situations do not arise due to mistakes in the application code, but they must be handled correctly. Otherwise, they could cause more serious problems.
2. Programmer errors
Programmer errors are mistakes in the logic or syntax of the program that can only be corrected by changing the source code. These types of errors cannot be handled because, by definition, they are bugs in the program. Some examples of programmer errors include:
Syntax errors, such as failing to close a curly brace.
Type errors when you try to do something illegal, such as performing operations on operands of mismatched types.
Bad parameters when calling a function.
Reference errors when you misspell a variable, function, or property name.
Trying to access a location beyond the end of an array.
Failing to handle an operational error.
Operational error handling
Operational errors are mostly predictable, so they must be anticipated and accounted for during the development process. Essentially, handling these types of errors involves considering whether an operation could fail, why it might fail, and what should happen if it does. Let’s consider a few strategies for handling operational errors in Node.js.
1. Report the error up the stack
In many cases, the appropriate action is to stop the flow of the program’s execution, clean up any unfinished processes, and report the error up the stack so that it can be handled appropriately. This is often the correct way to address the error when the function where it occurred is further down the stack such that it does not have enough information to handle the error directly. Reporting the error can be done through any of the error delivery methods discussed earlier in this article.
2. Retry the operation
Network requests to external services may sometimes fail, even if the request is completely valid. This may be due to a transient failure, which can occur if there is a network failure or server overload. Such issues are usually ephemeral, so instead of reporting the error immediately, you can retry the request a few times until it succeeds or until the maximum amount of retries is reached. The first consideration is determining whether it’s appropriate to retry the request. For example, if the initial response HTTP status code is 500, 503, or 429, it might be advantageous to retry the request after a short delay.
You can check whether the Retry-After HTTP header is present in the response. This header indicates the exact amount of time to wait before making a follow-up request. If the Retry-After
header does not exist, you need to delay the follow-up request and progressively increase the delay for each consecutive retry. This is known as the exponential back-off strategy. You also need to decide the maximum delay interval and how many times to retry the request before giving up. At that point, you should inform the caller that the target service is unavailable.
3. Send the error to the client
When dealing with external input from users, it should be assumed that the input is bad by default. Therefore, the first thing to do before starting any processes is to validate the input and report any mistakes to the user promptly so that it can be corrected and resent. When delivering client errors, make sure to include all the information that the client needs to construct an error message that makes sense to the user.
4. Abort the program
In the case of unrecoverable system errors, the only reasonable course of action is to log the error and terminate the program immediately. You might not even be able to shut down the server gracefully if the exception is unrecoverable at the JavaScript layer. At that point, a sysadmin may be required to look into the issue and fix it before the program can start again.
Preventing programmer errors
Due to their nature, programmer errors cannot be handled; they are bugs in the program that arise due to broken code or logic, which must subsequently be corrected. However, there are a few things you can do to greatly reduce the frequency at which they occur in your application.
1. Adopt TypeScript
TypeScript is a strongly typed superset of JavaScript. Its primary design goal is to statically identify constructs likely to be errors without any runtime penalties. By adopting TypeScript in your project (with the strictest possible compiler options ), you can eliminate a whole class of programmer errors at compile time. For example, after conducting a postmortem analysis of bugs, it was estimated that 38% of bugs in the Airbnb codebase were preventable with TypeScript.
When you migrate your entire project over to TypeScript, errors like “undefined
is not a function”, syntax errors, or reference errors should no longer exist in your codebase. Thankfully, this is not as daunting as it sounds. Migrating your entire Node.js application to TypeScript can be done incrementally so that you can start reaping the rewards immediately in crucial parts of the codebase. You can also adopt a tool like ts-migrate if you intend to perform the migration in one go.
2. Define the behavior for bad parameters
Many programmer errors result from passing bad parameters. These might be due not only to obvious mistakes, such as passing a string instead of a number, but also to subtle mistakes, such as when a function argument is of the correct type but outside the range of what the function can handle. When the program is running and the function is called that way, it might fail silently and produce a wrong value, such as NaN
. When the failure is eventually noticed (usually after traveling through several other functions), it might be difficult to locate its origins.
You can deal with bad parameters by defining their behavior either by throwing an error or returning a special value, such as null
, undefined
, or -1
, when the problem can be handled locally. The former is the approach used by JSON.parse()
, which throws a SyntaxError
exception if the string to parse is not valid JSON, while the string.indexOf()
method is an example of the latter. Whichever you choose, make sure to document how the function deals with errors so that the caller knows what to expect.
3. Automated testing
On its own, the JavaScript language doesn’t do much to help you find mistakes in the logic of your program, so you have to run the program to determine whether it works as expected. The presence of an automated test suite makes it far more likely that you will spot and fix various programmer errors, especially logic errors. They are also helpful in ascertaining how a function deals with atypical values. Using a testing framework, such as Jest or Mocha , is a good way to get started with unit testing your Node.js applications.
Uncaught exceptions and unhandled promise rejections
Uncaught exceptions and unhandled promise rejections are caused by programmer errors resulting from the failure to catch a thrown exception and a promise rejection, respectively. The uncaughtException
event is emitted when an exception thrown somewhere in the application is not caught before it reaches the event loop. If an uncaught exception is detected, the application will crash immediately, but you can add a handler for this event to override this behavior. Indeed, many people use this as a last resort way to swallow the error so that the application can continue running as if nothing happened:
javascript
// unsafe
process . on ( 'uncaughtException' , ( err ) => {
console . error ( err );
});
However, this is an incorrect use of this event because the presence of an uncaught exception indicates that the application is in an undefined state. Therefore, attempting to resume normally without recovering from the error is considered unsafe and could lead to further problems, such as memory leaks and hanging sockets. The appropriate use of the uncaughtException
handler is to clean up any allocated resources, close connections, and log the error for later assessment before exiting the process.
javascript
// better
process . on ( 'uncaughtException' , ( err ) => {
Honeybadger . notify ( error ); // log the error in a permanent storage
// attempt a gracefully shutdown
server . close (() => {
process . exit ( 1 ); // then exit
});
// If a graceful shutdown is not achieved after 1 second,
// shut down the process completely
setTimeout (() => {
process . abort (); // exit immediately and generate a core dump file
}, 1000 ). unref ()
});
Similarly, the unhandledRejection
event is emitted when a rejected promise is not handled with a catch
block. Unlike uncaughtException
, these events do not cause the application to crash immediately. However, unhandled promise rejections have been deprecated and may terminate the process immediately in a future Node.js release. You can keep track of unhandled promise rejections through an unhandledRejection
event listener, as shown below:
javascript
process . on ( 'unhandledRejection' , ( reason , promise ) => {
Honeybadger . notify ({
message : 'Unhandled promise rejection' ,
params : {
promise ,
reason ,
},
});
server . close (() => {
process . exit ( 1 );
});
setTimeout (() => {
process . abort ();
}, 1000 ). unref ()
});
You should always run your servers using a process manager that will automatically restart them in the event of a crash. A common one is PM2 , but you also have systemd
or upstart
on Linux, and Docker users can use its restart policy . Once this is in place, reliable service will be restored almost instantly, and you’ll still have the details of the uncaught exception so that it can be investigated and corrected promptly. You can go further by running more than one process and employ a load balancer to distribute incoming requests. This will help to prevent downtime in case one of the instances is lost temporarily.
Centralized error reporting
No error handling strategy is complete without a robust logging strategy for your running application. When a failure occurs, it’s important to learn why it happened by logging as much information as possible about the problem. Centralizing these logs makes it easy to get full visibility into your application. You’ll be able to sort and filter your errors, see top problems, and subscribe to alerts to get notified of new errors.
Honeybadger provides everything you need to monitor errors that occur in your production application. Follow the steps below to integrate it into your Node.js app:
1. Install the Package
Use npm
to install the package:
command
$ npm install @honeybadger-io/js --save
2. Import the Library
Import the library and configure it with your API key to begin reporting errors:
javascript
const Honeybadger = require ( '@honeybadger-io/js' );
Honeybadger . configure ({
apiKey : '[ YOUR API KEY HERE ]'
});
3. Report Errors
You can report an error by calling the notify()
method, as shown in the following example:
javascript
try {
// ...error producing code
} catch ( error ) {
Honeybadger . notify ( error );
}
For more information on how Honeybadger integrates with Node.js web frameworks, see the full documentation or check out the sample Node.js/Express application on GitHub.
Summary
The Error
class (or a subclass) should always be used to communicate errors in your code. Technically, you can throw
anything in JavaScript, not just Error
objects, but this is not recommended since it greatly reduces the usefulness of the error and makes error handling error prone. By consistently using Error
objects, you can reliably expect to access error.message
or error.stack
in places where the errors are being handled or logged. You can even augment the error class with other useful properties relevant to the context in which the error occurred.
Operational errors are unavoidable and should be accounted for in any correct program. Most of the time, a recoverable error strategy should be employed so that the program can continue running smoothly. However, if the error is severe enough, it might be appropriate to terminate the program and restart it. Try to shut down gracefully if such situations arise so that the program can start up again in a clean state.
Programmer errors cannot be handled or recovered from, but they can be mitigated with an automated test suite and static typing tools. When writing a function, define the behavior for bad parameters and act appropriately once detected. Allow the program to crash if an uncaughtException
or unhandledRejection
is detected. Don’t try to recover from such errors!
Use an error monitoring service, such as Honeybadger , to capture and analyze your errors. This can help you drastically improve the speed of debugging and resolution.
Conclusion
Proper error handling is a non-negotiable requirement if you’re aiming to write good and reliable software. By employing the techniques described in this article, you will be well on your way to doing just that.
Thanks for reading, and happy coding!
Support the Freshman blog
Lots of time and effort goes into creating all the content on this
website. If you enjoy my work, consider supporting what I do through a
financial donation. You can support the Freshman blog with a one-time or
monthly donation through one of the following channels: