Pizzly, our open-source project, is live on Product Hunt! 🚀

Customizing Errors in Node.js

Javascript's built in Error provides useful information, but can often feel lacking in clarity. One-size-fits-all is great for the language, but we can do better in our own apps. That's where custom errors come in.

You may have seen custom errors when using Node.js. Node's built in error types like AssertionError, RangeError, ReferenceError, SyntaxError, and SystemError are all extensions of the native Error class.

Using Error as a base gives you all the power of Javascript's implementation plus additional benefits. Here are just a few:

  • Custom error message formats.
  • Additional properties on the Error object.
  • Named errors to make debugging and conditional error handling easier.
  • Consistent errors for library consumers to reference.

Extending the Error class

To get started, let's use a generic example. We will extend the Error class and use super to inherit it's functions.

class CustomError extends Error {
  constructor(...params) {
    super(...params)
    // We're spreading `params` as a way to bring all of `Error`'s functionality in.
  }
}

Now, when you throw an error, you can do so with throw new CustomError("Something went wrong..."). It also gives you the ability to check errors against the type:

try {
  throw new CustomError("Something went wrong")
} catch (error) {
  if (error instance of CustomError) {
    // do something specifically for that type of error
  }
}

This alone doesn't do much, other than give you a new error type to call and check for. To better understand what can be done, let's look at what come standard on Error.

  • Error.name
  • Error.message
  • Error.prototype.toString()
  • Error.prototype.constructor()

Not much to work with is there? Aside from the constructor and the toString method, the name of the error and the message describing the error are the only parts that we're likely to use. In the example above, message is set by passing "something went wrong" as the argument when instantiating the error.

Fortunately, most javascript platforms like the browser and Node.js have added their own methods and properties on top of the ones listed. We'll focus on a few aspects of the Error implementation from V8, the Javascript engine powering Chrome and Node.js.

The two areas of interest are stack and captureStackTrace. As their names suggest, they allow you to surface the stack trace. Let's see what that looks like with an example.

class CustomError extends Error {
  constructor(...args) {
    super(...args)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CustomError)
    }
    this.name = "Our Custom Error"
  }
}

try {
  throw new CustomError("Something went wrong")
} catch (err) {
  console.error(err.stack)
}

In this example we are calling Error.captureStackTrace, if the platform supports it, to ensure that a complete trace is added to our custom error. We then throw the error from within the try block, which will pass control to the catch block.

If you run the code above, you'll see that the first line of the stack is the error's name and message.

Our Custom Error: Something went wrong

This error isn't very "deep", so the stack is mostly Node.js internals. Each line is one "frame" of the stack. They contain details about the location of the error within the codebase.

Now that we know how to make and call a custom error, let's make it useful.

Customizing Error Content

The custom error we've been working with is fine, but let's change it to make it more useful. The class will be CircuitError. We'll use it to provide custom errors coming from a circuit breaker. If you aren't familiar with the pattern, that's okay. The implementation details of the circuit breaker aren't important. What is important is that we can pass some information into the error, and it will present that information to the user.

A breaker has an "OPEN" state that doesn't allow anything to pass through it for a fixed amount of time. Let's set our CircuitError up to contain some details that might be useful to the functions that receive it when the state is OPEN.

class CircuitError extends Error {
  // 1
  constructor(state, nextAttempt, ...params) {
    super(...params)

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CircuitError)
    }
    this.name = "CircuitError"
    this.state = state
    // 2
    this.message = `The Circuit is ${state}.`
    // 3
    if (nextAttempt) {
      this.timestamp = Date.now()
      this.nextAttempt = nextAttempt
      this.message += ` Next attempt can be made in ${this.nextAttempt -
        this.timestamp}ms.`
    }
  }
}

Our updated error does a few new things, all within the constructor. The arguments passed have changed (1) to include the state of the circuit breaker, as well as a nextAttempt timestamp to indicate when the breaker will start trying requests again.

It then sets a few new properties and updates the message depending on the values presented. We can test it by throwing a new version of this error.

try {
  throw new CircuitError("OPEN", Date.now() + 8000)
} catch (err) {
  console.error(err)
}

Now when we throw the error, it takes the state as the first argument and a timestamp as the second. For this demo we're passing in time 8000 milliseconds in the future. Notice that we're also logging the error itself, rather than just the stack.

Running the code will result in something like the following:

CircuitError [Circuit Error]: The Circuit is OPEN. Next attempt can be made in 6000ms.
    at Object.<anonymous> (/example.js:21:9)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1103:10)
    at Module.load (internal/modules/cjs/loader.js:914:32)
    at Function.Module._load (internal/modules/cjs/loader.js:822:14)
    at Function.Module.runMain (internal/modules/cjs/loader.js:1143:12)
    at internal/main/run_main_module.js:16:11 {
  name: 'Circuit Error',
  state: 'OPEN',
  message: 'The Circuit is OPEN. Next attempt can be made in 6000ms.',
  timestamp: 1580242308919,
  nextAttempt: 1580242314919
}

The result is the error name followed by the message on the first line. Then we have the rest of the stack, followed by all of the properties we've set in the CircuitError. Our consumers of this error could use that data to react to the error. For example:

try {
  throw new CircuitError("OPEN", Date.now() + 8000)
} catch (err) {
  customLogger(err)
  if (err.state === "OPEN") {
    handleOpenState(err.nextAttempt)
  }
}

Instead of a generic error, we now have something better suited to the needs of our application.

Reacting to failures

Custom errors are a great way to react to failures. It is common to assume that all errors are unexpected, but by providing useful errors you can make their existence easier to handle. By making use of custom errors, not only can we provide users of our applications with valuable data, but we also present the ability to respond to error types through the instance of condition.

At Bearer, we use custom errors to standardize responses in our code, provide just the information you need in our client, and make our errors actionable.

How are you using custom errors in your applications? Connect with us @BearerSH and let us know.

You may also like

Consume APIs. Stay in Control.

Monitor, track performance, detect anomalies, and fix issues on your critical API usage.

Learn more Schedule a Demo