Stackify is now BMC. Read theBlog

Node.js Error Handling Best Practices: Ship With Confidence

By: Lou
  |  May 1, 2023
Node.js Error Handling Best Practices: Ship With Confidence

Node.js error handling isn’t a walk in the park. When deploying applications into production, we want to know that all code has been tested for all possible complex scenarios.

We can, however, avoid issues by preparing our code to properly handle errors. To do so, we need to understand:

Today we’re going to take you through the above items and give you a tour of JavaScript’s main quirks for better insight and understanding. Then, we’ll discuss best practices and common stumbling blocks that developers often struggle with when dealing with Node.js error handling.

So let’s get to it.

The Importance of Node.js Error Handling

First let’s address the big question: why bother handling errors at all?

Imagine you’re using Node.js to build a RESTful web API. You have users who are sending requests for data from your server. All good so far, right?

Let’s look at an example. Imagine you’re using Node.js to build a RESTful web API. You have users who are sending requests for data from your server. All good so far, right?

With anyone in the world throwing requests at our program, it’s only a matter of time before values get into our program that we didn’t expect. And when we get these values, effectively responding to our users means being as descriptive as possible about the error. If we don’t have proper Node.js error handling, it’s likely we’ll throw a generic error message. So the user will see the error message “Unable to complete request.”

This is not very useful.

Furthermore, it’s not just the user experience that dictates why we should be savvy with our Node.js error handling. If we want our program to be secure, resilient, high-performing and bug-free, Node.js error handling is a must.

Understanding Types of Errors in Node.js

In the context of Node.js, errors are generally classified into two categories: operational errors and programmer errors, which are also known as functional errors.

Operational Errors

Operational errors in Node.js refer to runtime errors when an application encounters an unhandled exception or another code issue. These errors can be challenging to diagnose, as memory leaks, infinite loops, incorrect system configurations, or a combination of these issues often cause them.

The most common way of dealing with operational errors is by implementing a robust error-handling strategy designed to capture any error and provide insight into what went wrong so developers can address it quickly and efficiently.

Functional Errors

Functional errors refer to bugs in the application code that prevent it from functioning as expected. These types of errors usually require more effort to identify than operational ones since they manifest themselves in unexpected ways or only at certain times during the application’s lifecycle. Examples of functional errors include incorrect logic or calculations, missed use cases, and other miscellaneous coding mistakes such as typos or misconfigurations.

Identifying functional errors requires developers to look deeper into their codebase and use debugging tools like breakpoints and stack traces to pinpoint precisely where the issue lies within their application.

Best Practice 1: Always Use the Error Object for Consistent Error Handling

The first thing to know about Node.js error handling is the error object.

You might have seen some code that looks like this:

throw new Error('database failed to connect');

That may sound pretty overwhelming.

For simplicity, let’s break it down. Two distinct things are happening here: the error object is being created and is being thrown. Let’s start by looking at the error object and how it works and get back to the throw keyword a little later.

Despite their scary exterior, error objects are pretty straightforward. The error object is an implementation of a constructor function that uses a set of instructions (the arguments and constructor body itself) to create an object. That’s it. The built-in error constructor is simply a unified way of creating an error object.

What are error objects, anyway? Why do they need to be uniform?

These are important questions, so let’s get to them.

Anatomy of an error object

The first argument for a native error object is its description. The description is the human-readable string of your error object. It’s what pops up in your console when something goes awry.

Second, error objects also have a name property, which is the computer-readable part of the object. When you use the native error object, the name property defaults to the generic “Error.” However, you can also create your own. The best way to do this is by extending the native error object like so:

class FancyError extends Error {
    constructor(args){
        super(args);
        this.name = "FancyError"
    }
}

console.log(new Error('A standard error'))
// { [Error: A standard error] }

console.log(new FancyError('An augmented error'))
// { [Your fancy error: An augmented error] name: 'FancyError' }

We mentioned earlier that we want our error objects to be uniform. That’s because when we throw an error, it helps to have consistency in the thrown object. If we have consistency in our objects, error handling is easier (more on this later).

Now let’s discuss throw, our next piece in the puzzle.

Best Practice 2: Use the Throw Statement to Stop Program Execution and Locate the Error Source

Use the throw statement to stop program execution and locate the error source. Creating an error object is not the end of the story and only metaphorically primes our error for sending. How we hit send on our error is by throwing. But what does it mean to throw? And what does that mean for our program?

Throw really does two things: it stops the program, and it finds a catch to execute. Let’s examine these ideas one at a time.

Throw really does two things: it stops the program, and it finds a catch to execute.

The Mechanics of Throwing Errors

When JavaScript finds a throw keyword, the first thing it does is stop dead in its tracks. This event prevents any more functions from running. By stopping like this, it mitigates the risk of any further errors occurring and helps debug programs easily.

With the program halted, JavaScript will begin to trace the daisy chain of functions that were called in order to reach a catch statement. This chain is called the call stack (don’t worry – we’ll get to the call stack soon). The nearest catch that JavaScript finds is where the thrown exception emerges. If no try/catch is found, the exception throws, and the Node.js process exits, causing the server to restart.

Throwing by example

So far, we’ve been quite theoretical. Let’s take a look at an example:

function doAthing() {
    byDoingSomethingElse();
}

function byDoingSomethingElse() {
    throw new Error('Uh oh!');
}

function init() {
    try {
        doAthing();
    } catch(e) {
        console.log(e);
        // [Error: Uh oh!]
    }
}

init();

Here we can see that the init function has try/catch error handling in place. It calls a function, which calls another function, which in turn throws an error. It is at the point of error when the program halts and begins tracing the function that threw the error. Eventually it gets back to the init function and executes the catch statement. In the catch statement, we can decide to take an action, suppress the error, or even throw another error (to propagate upward).

Best Practice 3: Use the Call Stack to Trace Errors Back to Their Source

Use the call stack to trace errors back to their source. What we’re seeing in the above example is a worked example of the call stack. In order to function (like most languages), JavaScript utilizes a concept known as the call stack. But how does a call stack work?

What is a Call Stack?

Whenever a function is called, it’s put onto the stack; when it’s returned, it’s removed from the stack. It is from this stack that we derive the name “stack traces,” which you might have heard of. These are the big, scary-looking messages that we sometimes see when something is buggy in our program.

They often look like this:

Error: Uh oh!
at byDoingSomethingElse (/filesystem/aProgram.js:7:11)
at doAthing (/filesystem/aProgram.js:3:5)
at init (/filesystem/aProgram.js:12:9)
at Object.<anonymous> (/filesystem/aProgram.js:19:1)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)

At this point, you might be wondering how a call stack helps us with Node.js error handling. Let’s talk about the importance of call stacks.

The Importance of Call Stacks in Error Handling

Your call stack provides your breadcrumbs, helping you trace back the way in which you came. The call stack also enables you to trace the origin of a failure so you can easily see which functions were called. In addition, it helps trace when the run-up to an error was created.

But wait, there’s some nuance to all this call stack stuff.

Your call stack provides your breadcrumbs, helping you trace back the way in which you came.

Where this gets hairy is the topic of anonymous functions, or functions that don’t have names. Why, you ask, would we have functions without names? Sometimes in our programs, we want to define small, throw-away functions that do a small thing. We don’t want to labor ourselves with the task of naming them, but it’s these anonymous functions that can cause us all kinds of headaches. An anonymous function removes the function name from our call stack, which makes our call stack significantly harder to use.

Note that naming functions in JavaScript is not that straightforward. So, let’s take a quick look at the different ways that you can define functions and address some of the pitfalls in function naming.

Best Practice 4: Name Your Functions to Improve Stack Trace Readability

Name your functions to improve stack trace readability. How do we get around the problem of anonymous functions? The answer is to name your functions.

Why does this work? It’s because naming functions in Node.js helps us with our stack traces. If we go through our code and we change our anonymous functions to be named ones, we get much better stack traces.

To understand function naming, let’s look at some examples:

// Anonymous function
const one = () => {};

// Anonymous functions
const two = function () {};

// Explicitly named function
const three = function explicitFunction() {};

Here we’ve got three example functions.

The first is a lambda (sometimes called a fat arrow function). Lambda functions are anonymous by virtue. Don’t get confused. The variable name “one” is not the function name. The function name is optionally passed after the keyword “function.” But in this example, we’re not passing anything at all, so our function is anonymous.

Note: It doesn’t help that some JavaScript runtimes, like V8, can sometimes guess the name of your function. This happens even if you don’t give it one. But without digressing too much, you don’t want to rely on this functionality because it’s not consistent.

Second, we’ve got a function expression. This is very similar to the first. It’s an anonymous function, but simply defined with the function keyword rather than the fat arrow syntax.

Finally, we have a variable declaration with an apt name: explicitFunction. It shows that the only function that is appropriately named is when you use the function keyword and pass the (optional) name. It’s generally a best practice to provide this name wherever you can to have more readable stack traces.

Best Practice 5: Handle Asynchronous Code Errors in Node.js Using Promises and Async/Await

Handle asynchronous code errors using promises and async/await. At this point, you’re an aficionado of the error object, throw keyword, call stacks and function naming. So, let’s turn our attention to the curious case of handling asynchronous errors. Why? Because they don’t behave as you’d expect, and asynchronous programming is essential to every Node.js programmer.

Before we get too deep, let’s evaluate how JavaScript handles async tasks, and why we even need to do this.

JavaScript’s Single-Threaded Nature

JavaScript is a single-threaded programming language which in plain English, means that JavaScript runs using a single processor. By virtue of having a single processor, we get blocking, or conversely, non-blocking code. Blocking code refers to whether your program will wait for an async task to be complete before doing anything else. Whereas non-blocking code refers to where you register a callback to perform when the task completes.

Essentially, it’s worth mentioning that there are two main ways you can handle async in JavaScript: promises or callbacks. We’re deliberately ignoring async/wait here to avoid confusion because it’s simply sugar on top of promises.

5.1 Getting Started with Promises

Promises allow you to handle async operations with .then() and .catch(). For the purpose of this article, we’ll focus on promises. There is considerable industry consensus that for application coding, promises beats callbacks in terms of programming style and effectiveness. So for this article, we’ll ignore the callback pattern and assume that you’re going to opt for promises instead. A fine choice, we might add.

Note: Luckily for you, there are many ways to convert your callback-based code into promises. For example, you can use a utility like the built-in promisify or wrap your callbacks in promises like so:

var request = require('request'); //http wrapped module
function requestWrapper(url, callback) {
    request.get(url, function (err, response) {
        if (err) {
            callback(err);
        } else {
            callback(null, response);
        }
    })
}

Managing Errors with Promises

Okay, so we’ve doubled down on promises. But what does this mean for our Node.js error handling?

Let’s take a look at the anatomy of a promise.

A promise in JavaScript is an object that represents a future value. Promises allow us to model asynchronous code like synchronous code by using the Promise API (which we’ll see later). It’s also worth noting that a promise usually comes in a chain, where one action executes, then another and so on.

But what does this all mean for Node.js error handling?

Promises handle errors quite elegantly and will catch any errors that preceded it in the chain. This is great for Node.js programmers because we can handle many errors in many functions within a single handler. Let’s take a look a the code below:

function getData() {
    return Promise.resolve('Do some stuff');
}

function changeDataFormat(){
    // ...
}

function storeData(){
    // ...
}

getData()
    .then(changeDataFormat)
    .then(storeData)
    .catch((e) => {
        // Handle the error!
    })

Here we see how you can roll up your error handling for all three different functions into one. Essentially, this behaves as if it’s wrapping all of your functions in a synchronous try/catch.

5.2 Use Try/Catch Blocks to Handle Synchronous Code Errors

Use try/catch blocks to handle synchronous code errors. At this point, you might be wondering whether adding a catch to your promises is optional. Yes it’s optional, but you should always provide a catch handler.

Why? Because there are many ways your asynchronous calls can fail. Our code might timeout, it could have network issues or there might be a hardware failure. For all of these reasons, you should always instruct your program what to do in the case of a promise failure.

Remember the golden rule: always handle promise rejections.

5.3 Handling Errors in Asynchronous Functions with async/await

The async/await syntax provides a more synchronous-looking way to handle async cod. We’re nearing the end now on our journey through Node.js error handling. But it’s the right time to identify a pretty big pitfall of async code and the try/catch statement.

You might have been wondering why the promise exposes a catch method and why we can’t just wrap our promise implementation in a try/catch. If you were to do this, the results would not be as you expect.

Let’s take an example:

try {
    throw new Error();
} catch(e) {
    console.log(e); // [Error]
}

try {
    setTimeout(() => {
        throw new Error();
    }, 0);
} catch(e) {
    console.log(e); // Nothing, nada, zero, zilch, not even a sound
}

Give that a good look. Do you see what’s happening here?

Try/catch is by default synchronous. That means that if an asynchronous function throws an error in a synchronous try/catch block, no error throws.

That’s definitely not what we want.

In summary, if you’re handling async error handling, you should really use the promises catch handler, which will allow you to effectively handle the async errors. But if you’re dealing with synchronous code, the try/catch will do just fine.

Best Practice 6: Implementing Tooling to Handle Errors

If an exception throws, but no programmer sees it, was it even thrown?

— Ancient Zen Buddhist programming proverb

Implement tooling to monitor and handle errors. It’s not enough to just handle errors within your code; you must also ensure that you’re aware of errors when they occur in production. Handling errors effectively in your application is only half the battle. The other half is knowing when errors occur and collecting as much information as possible to address them promptly.

Handling errors in your program deals only with the things you know about. As much as we can write flawless code, mistakes happen. Rather than trying to prevent all failures from ever happening, we should try to know when a failure happens. We should also get the most information we can about the problem.

And that’s where an application performance monitoring tool comes into play. APMs come in all different shapes and sizes. Some analyze your log data or infrastructure; some capture and manage uncaught exceptions. When it comes to error handling, it makes sense to use an APM to get visibility into your application. There’s no point spending all this time optimizing stack traces, function names and the like if all this rich data is thrown into some log file on a desolate server.

By utilizing an APM such as Retrace, you can get insights into how your program is performing. You can also see how your database queries execute, the load on your servers and so on. All of this can drastically help you improve the speed of debugging and remediation time.

Retrace collects a lot of contextual data about what is occurring when an error is thrown. This data can be very useful for error reporting purposes. You can get a list of your customers or users being affected by an error, enabling you to see every unique stack trace, URL or method name impacted by the error. Download your free two week trial of Retrace today.

Node.JS Error Hndling and Confidence in Shipping

And that’s all we have for today. We hope that was a useful introduction to error handling in Node.js. At this point, you should feel more comfortable with the idea of throwing error objects and catching them in either synchronous or asynchronous code. Your applications will be significantly more robust and ready for production.

You may also want to try Stackify’s free, real-time code profiler, Prefix, to write better code on your workstation. Prefix works with .NET, Java, PHP, Node.js, Ruby, and Python.

Until next time, handle your errors and ship with confidence!

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]