DEV Community

Cover image for Node.js Error Handling: Tips and Tricks
Kayode for AppSignal

Posted on • Originally published at blog.appsignal.com

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:

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, () => {});
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

try {
  doSomethingSynchronous();
} catch (err) {
  console.error(err.message);
}
Enter fullscreen mode Exit fullscreen mode

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:

function doSomeThingAsync() {
  return new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("some error")), 5000)
  );
}

try {
  doSomeThingAsync();
} catch (err) {
  console.log("ERROR: ", err);
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
});
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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();
Enter fullscreen mode Exit fullscreen mode

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:

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);
  });
Enter fullscreen mode Exit fullscreen mode

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:

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
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)