javascript

Node.js Error Handling: Tips and Tricks

Kayode Oluwasegun

Kayode Oluwasegun on

Node.js Error Handling: Tips and Tricks

As unpleasant as they are, errors are crucial to software development.

When developing an application, we usually don't have full control over the parties interacting with a program and its hosts (including operating system versions, processors, and network speed).

It's important you have an error reporting system to diagnose errors and make errors human-readable.

In this post, we'll first look at the two common types of errors. We'll then explore how to handle errors in Node.js and track them using AppSignal.

Let's get started!

Common Types of Errors

We will discuss two types of errors in this article: programming and operational errors.

Programming Errors

These errors generally occur in development (when writing our code). Examples of such errors are syntax errors, logic errors, poorly written asynchronous code, and mismatched types (a common error for JavaScript).

Most syntax and type errors can be minimized by using Typescript, linting, and a code editor that provides autocomplete and syntax checking, like Visual Studio Code IntelliSense.

Operational Errors

These are the kind of errors that should be handled in our code. They represent runtime problems that occur in operational programs (programs not affected by programming errors). Examples of such problems include invalid inputs, database connection failure, unavailability of resources like computing power, and more.

For instance, let's say you write a program using the Node.js File System module (fs) to do some operations on a jpeg file. It’s a good practice to enable that program to handle most common edge cases (like uploading non-image files). However, a lot of problems may occur in your program depending on where or how fs is used, especially where you have no control over them (in the processors or Node version on the host, for example).

Errors in Node.js

Errors in Node.js are handled as exceptions. Node.js provides multiple mechanisms to handle errors.

In a typical synchronous API, you will use the throw mechanism to raise an exception and handle that exception using a try...catch block. If an exception goes unhandled, the instance of Node.js that is running will exit immediately.

For an asynchronous-based API, there are many options to handle callbacks — for instance, the Express.js default error-first callback argument.

Custom Error: Extending the Error Object in Node.js

All errors generated by Node.js will either be instances of — or inherit from — the Error class. You can extend the Error class to create categories of distinct errors in your application and give them extra, more helpful properties than the generic error.message provided by the error class. For instance:

jsx
class CustomError extends Error { constructor(message, code, extraHelpFulValues) { super(message); this.code = code; this.extraHelpFulValues = extraHelpFulValues; Error.captureStackTrace(this, this.constructor); } helpFulMethods() { // anything goes like log error in a file, } } class AnotherCustomError extends CustomError { constructor(message) { super(message, 22, () => {}); // ... } }

Handling Errors in Node.js

Once an exception is unhandled, Node.js shuts down your program immediately. But with error handling, you have the option to decide what happens and handle errors gracefully.

Try Catch in Node

try...catch is a synchronous construct that can handle exceptions raised within the try block.

jsx
try { doSomethingSynchronous(); } catch (err) { console.error(err.message); }

So if we have an asynchronous operation within the try block, an exception from the operation will not be handled by the catch block. For instance:

jsx
function doSomeThingAsync() { return new Promise((resolve, reject) => setTimeout(() => reject(new Error("some error")), 5000) ); } try { doSomeThingAsync(); } catch (err) { console.log("ERROR: ", err); }

Even though the doSomethingAsync function is asynchronous, try...catch is oblivious to the exception raised in the asynchronous function.

Some versions of Node.js will complain about having unhandled exceptions. The -unhandled-rejections=mode is a very useful flag to determine what happens in your program when an unhandled exception occurs.

Let's now look at more ways to handle exceptions raised in asynchronous operations.

Error-First Callback Error Handling

This is a technique for handling asynchronous errors used in most Callback APIs. With this method, if the callback operation completes and an exception is raised, the callback's first argument is an Error object. Otherwise, the first argument is null, indicating no error.

You can find this pattern in some Node modules:

jsx
const fs = require("fs"); fs.open("somefile.txt", "r+", (err, fd) => { // if error, handle error if (err) { return console.error(err.message); } // operation successful, you can make use of fd console.log(fd); });

So you can also create functions and use Node’s error-first callback style, for instance:

jsx
function doSomethingAsync(callback) { // doing something that will take time const err = new CustomerError("error description"); callback(err /* other arguments */); }

Promise Error Handling

The Promise object represents an asynchronous operation's eventual completion (or failure) and its resulting value.

We mentioned above how the basic try...catch construct is a synchronous construct. We proved how the basic catch block does not handle exceptions from these asynchronous operations.

Now we'll discuss two major ways to handle errors from asynchronous operations:

  • Using async/await with the try...catch construct
  • Using the .catch() method on promises

Async/Await Error Handling with Try Catch

The regular try...catch is synchronous. However, we can make try...catch handle exceptions from asynchronous operations by wrapping try...catch in an async function. For example:

jsx
function doSomeThingAsync() { return new Promise((resolve, reject) => setTimeout(() => reject(new Error("some error")), 5000) ); } async function run() { try { await doSomeThingAsync(); } catch (err) { console.log("ERROR: ", err); } } run();

When running this code, the catch block will handle the exception raised in the try block.

Using the .catch() Method

You can chain a .catch to handle any exception raised in Promise. For instance:

jsx
function doSomeThingAsync() { return new Promise((resolve, reject) => setTimeout(() => reject(new Error("some error")), 5000) ); } doSomeThingAsync() .then((resolvedValue) => { // resolved the do something }) .catch((err) => { // an exception is raised console.error("ERROR: ", err.message); });

Event Emitters

EventEmitter is another style of error handling used commonly in an event-based API. This is exceptionally useful in continuous or long-running asynchronous operations where a series of errors can happen. Here's an example:

jsx
const { EventEmitter } = require("events"); class DivisibleByThreeError extends Error { constructor(count) { super(`${count} is divisible by 3`); this.count = count; } } function doSomeThingAsync() { const emitter = new EventEmitter(); // mock asynchronous operation let count = 0; const numberingInterval = setInterval(() => { count++; if (count % 3 === 0) { emitter.emit("error", new DivisibleByThreeError(count)); return; } emitter.emit("success", count); if (count === 10) { clearInterval(numberingInterval); emitter.emit("end"); } }, 500); return emitter; } const numberingEvent = doSomeThingAsync(); numberingEvent.on("success", (count) => { console.log("SUCCESS: ", count); }); numberingEvent.on("error", (err) => { if (err instanceof DivisibleByThreeError) { console.error("ERROR: ", err.message); } // other error instances });

In the sample above, we create a custom error called DivisibleByThreeError and also a function that creates an event emitter. The function emits both success and error events at different intervals, running a counter from 1 to 10 and emitting an error when the counter is divisible by 3.

We can listen to error events, determine the type of error, and then act accordingly or end the program.

Using AppSignal to Track Node.js Errors

Your Node.js project will usually run on various hosts (such as different operating systems and processors) with different network setups and network speed structures, for example.

An Application Performance Management (APM) tool like AppSignal monitors your application, so you can easily track error alerts and keep your errors under control.

From the issue list, you can dig into errors and see a description of each incident, as well as when it happened in your app. Here's how it looks in AppSignal:

errors-details

Get started with AppSignal for Node.js.

Wrap Up

In this article, we explored errors in Node.js and touched on the many methods we can use to handle errors, including:

  • Using the try...catch block
  • An error-first callback style
  • Using try...catch with async/await
  • Promise with .catch
  • Event emitters

We also discussed using AppSignal to track errors.

Happy coding!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Kayode Oluwasegun

Kayode Oluwasegun

Our guest author Kayode is a full-stack developer specializing in the development of web applications using React, Node.js, TypeScript, and more. He also enjoys writing articles about these technologies.

All articles by Kayode Oluwasegun

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps