Node.js Error Handling Best Practices: Ship With Confidence

Lou Bichard Developer Tips, Tricks & Resources

Node.js error handling isn’t a walk in the park. When putting our code into production, we want to know that it’s battle-tested and hardened for all the complexities that production will throw at it.

In order to handle our errors properly, we need to understand:

Wowza—that’s a lot. But it’s your lucky day. Today we’re going to go through all of the above. To give us a full understanding, we’ll do a tour of JavaScript’s main quirks. Then we’ll discuss best practices and look at the main stumbling blocks that developers new to Node.JS error handling often struggle with.

Sound good to you? Let’s get to it.

Node.JS Error Handling: Why Bother?

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

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 we get some values into our program that we didn’t expect. And when we get these values, we want to effectively respond to our users with as much description as we can. 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.

But it’s not just user experience that dictates why we should be savvy with our Node.js error handling. If we want our program to be secure, resilient, performant, and bug-free, we should have good Node.JS error handling so we need to know what that looks like.

Error Object

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');

Hmm, that seems 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 (don’t worry—we’ll get back to the throw keyword soon).

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? And why do they need to be uniform?

These are important questions; 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.” , but you can 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' }

I 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, we’ll be able to handle our errors easier (more on this later).

At this point, you might be thinking, “Whoa! Hold up! We’ve not talked about throw yet!” And you’d be right—throw is the next piece in the puzzle.

Uh-oh, an error—throw it!

We’ve now talked briefly about the error object. But beware: creating an error object isn’t the end of the story for us. Creating an error object only metaphorically primes our error for sending, but it doesn’t hit send. So how do we hit send on our error? 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.

When JavaScript finds a throw keyword, the first thing it does is stop dead in its tracks, which prevents any more functions from running. By stopping like this, it mitigates the risk of any further errors occurring and helps us not to get the state of our program all twisted.

With the program halted, JavaScript will begin to look back up the daisy chain of functions that were called in order to reach a catch statement. This daisy 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 will emerge. If no try/catch is found, the exception throws, and the Node.js process will exit, causing the server to restart.


Stackify Loves Developers

Throwing by example

So far, we’ve been quite theoretical, so let’s 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 searching upward for 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).

Call stack

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?

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 pretty wrong 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)

So you might be wondering, “OK, this is getting pretty technical now. How does a call stack help us with Node.JS error handling?” Let’s talk about the importance of call stacks.

Your call stack is your breadcrumbs; it will help you trace back the way in which you came. It allows you to trace the origin of a failure so you can easily see which functions were called—and when—in the run-up to an error being created.

But wait—hold up—there’s some nuance to all this call stack stuff.

Where this gets hairy is the topic of anonymous functions, or functions that don’t have names. That seems pretty odd—why would we have functions without names? Sometimes in our programs we want to define small, throw-away functions that do a small thing, and 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.

But 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.

How to name functions

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, 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.

Here you can see that the only function that is appropriately named is when you use the function keyword and pass the (optional) name. It’s generally best practice to provide this name wherever you can to have more readable stack traces.

Handling async (callback) errors

Now that you’re an aficionado of the error object, throw keyword, call stacks, and function naming, 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 the Node.js programmer.

Before we get too deep, let’s take a step back and understand how JavaScript handles async tasks—and why we even need to do this.

JavaScript is a single-threaded programming language, which in 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.

At this point, it’s worth mentioning that there are two main ways you can handle async in JavaScript: promises or callbacks. (I’m deliberately ignoring async/wait here to avoid confusion, because it’s simply sugar on top of promises.)

For the purpose of this article, we’ll focus on promises. There is a fair amount of industry consensus that for application code promises beats callbacks when it comes to 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, I 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);
        }
    })
}

We’ll handle this error, I promise!

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 are super awesome because they allow us to model asynchronous code like synchronous code through the use of 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, then another, and then another (more on this later).

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, which is great for Node.js programmers, because it means that we can handle many errors in many functions in a single handler. As follows:

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 the three different functions all into one. Essentially this behaves as it would wrapping all of your functions in a synchronous try/catch.

To catch or not to catch promises?

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.

The perils of async try/catch

We’re nearing the end now on our journey through Node.js error handling. But it’s the right time, I think, to raise 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 try doing 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.

Wait, say what?! That’s definitely not what we want.

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


Stackify Loves Developers

Handling errors With tooling

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

— Ancient Zen Buddhist programming proverb

I’ve got some good news and some bad news. If you’re handling errors as best you can in your application, awesome! That’s the good news. The bad news is that you’re only halfway there.

What do I mean? Well, handling errors in your program deals only with the things you know about. As much as I believe in you to write flawless code, mistakes happen. Rather than trying to prevent all failures from ever happening, we should try to know when a failure happens and get the most information we can about the problem.

And that’s where an APM comes in. An APM is an application performance monitoring tool. 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, how your database queries execute, the load on our servers, and so on. All of this can drastically help us improve the speed of debugging and remediation time.

Retrace collects a lot of context 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 and see every unique stack trace, URL, or method name that are all impacted by the error.

Node.JS Error handling and confidence in shipping

And that’s all I have for today. I hope that was a useful introduction to error handling in Node.js and that you now feel more at home with the idea of throwing error objects and catching them in either synchronous or asynchronous code. Now your applications should be significantly more robust and ready for production. So no more sweating and worrying about your application’s performance in production.

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

 

About Lou Bichard

Lou Bichard is a JavaScript full stack engineer with a passion for culture, approach, and delivery. He believes the best products emerge from high performing teams and practices. Lou is a fan and advocate of old-school lean and systems thinking, XP, continuous delivery, and DevOps.
Improve Your Code with Retrace APM

Stackify’s APM tools are used by thousands of .NET, Java, and PHP developers all over the world.
Explore Retrace’s product features to learn more.