Node.js Error Handling: Tips and Tricks
Tags: node
Published: 2022-11-23 14:59:45

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](https://www.typescriptlang.org/docs/), linting, and a code editor that provides autocomplete and syntax checking, like [Visual Studio Code IntelliSense](https://code.visualstudio.com/docs/editor/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`](https://nodejs.org/api/cli.html#cli_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](https://nodejs.org/api/events.html#events_class_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](https://blog.appsignal.com/images/blog/2022-11/error-details.png)

[Get started with AppSignal for Node.js](https://www.appsignal.com/nodejs).

## 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](https://blog.appsignal.com/javascript-sorcery) 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](https://www.appsignal.com/nodejs).**