Taka’s blog

A software engineer's blog who works at a start-up in London

Use JavaScript Custom Errors Better

In the JavaScript world, pitfalls are everywhere; one of them is error handling.

This post introduces a good way of using JavaScript's custom errors.

TL;DR

necojackarc/extensible-custom-error enables you to define handy extensible custom errors easily, which can take either/both an error object and/or an error message.

const ExtensibleCustomError = require('extensible-custom-error');

class MyError extends ExtensibleCustomError {}

new MyError('message'); // Take a message
new MyError(error); // Take an error
new MyError('message', error); //  Take a message and an error

Stack traces in error objects get merged so that you won't lose any error details:

throw new MyError('Unlimited Blade Works', error);
MyError: Unlimited Blade Works
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:101:11)
Error: Have withstood Pain to create many Weapons
    at throwBuiltinError (/home/necojackarc/custom_error.js:94:9)
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:99:5)
    at main (/home/necojackarc/custom_error.js:107:5)
    at Object.<anonymous> (/home/necojackarc/custom_error.js:113:1)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)

Default Error Handling with JavaScript

JS's try/catch is a bit inferior to others'; you can use only one catch clause. To handle specific errors, MDN advise a use of instanceof like the following:

try {
  throw new TypeError();
} catch (e) {
  if (e instanceof TypeError) {
    console.log('Caught a Type Error')
  } else {
    console.log('Others');
  }
}

You'll see Caught a Type Error when you run it.

You can do the same thing with Promises:

const rejectPromise = () => new Promise((resolve, reject) => {
  reject(new TypeError());
});

rejectPromise().catch((error) => {
  if (error instanceof TypeError) {
    console.log('Caught a TypeError');
  } else {
    console.log('Others');
  }
});

This will also spit out Caught a TypeError. With async/await, you can use regular try/catch, so I don't show examples here.

Pitfalls with custom errors that extend the default Error

Let's see what will happen when you define custom errors just by extending the default error object.

Pitfall 1: Data gets lost with a simple inheritance

Custom errors that extend Error don't work well:

// Incomplete costom error
class MyError extends Error {}

function throwError() {
  throw new MyError('Cutrom error');
}

try {
  throwError();
} catch (error) {
  if (error instanceof MyError) {
    console.log(error);
  } else {
    console.log('Non-custom error');
  }
}

By running the code above, you'll see:

Error: Custom error
    at throwError (/home/necojackarc/custom_error.js:6:9)
    at Object.<anonymous> (/home/necojackarc/custom_error.js:10:3)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)
    at bootstrap_node.js:609:3

instanceof works properly but the custom error name, MyError, has got lost.

You can get around this issue by overwriting the name property within constructor, or using Error.captureStackTrace available on V8.

class MyError extends Error {
  constructor(...args) {
    super(...args);

    // `this.name = this.constructor.name;` can work, but
    // setting `enumerable` `false` gets it closer to a built-in error
    Object.defineProperty(this, 'name', {
      configurable: true,
      enumerable: false,
      value: this.constructor.name,
      writable: true,
    });

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, MyError);
    }
  }
}

Replace the old custom error with this one and run the same code above, then you will see:

MyError: Custom error
    at throwError (/home/necojackarc/custom_error.js:27:9)
    at Object.<anonymous> (/home/necojackarc/custom_error.js:31:3)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)
    at bootstrap_node.js:609:3

Now it works fine.

MDN mentions this solution as well.

Pitfall 2: The constructor only takes a message

Of course, you can pass anything to them because you are using JavaScript, however, the constructors of the error objects only expect to receive a message, not an error object.

In other words, if you wrap a built-in error in your custom error like the following, you won't get an expected result.

function throwError() {
  throw new Error('Built-in error');
}

try {
  throwError();
} catch (error) {
  throw new MyError(error);
}

When you run this, you'll see:

MyError: Error: Built-in error
    at Object.<anonymous> (/home/necojackarc/custom_error.js:31:9)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)
    at bootstrap_node.js:609:3

It looks good at first glance, but you've lost some of the stack traces.

You can see the same output when you do error.toString(). The output of error.toStirng() is an error name and a message, so it'll be Error: Built-in error.

In short, stack traces are ignored when you pass an error object.

To cope with this issue, you need to add some logic to the constructor to merge the given stack traces to the ones generated here.

With JavaScript built-in errors, as I mentioned above, you can't pass both a message and an error like throw new myError('message', error) unlike Java.

Summary of the Pitfalls around JS Custom Errors

  • Just extending Error isn't good enough to define custom errors
  • Wrapping an error isn't possible by default and some data gets lost

Ideal World

In the ideal world, you can define your own errors like:

class MyError extends Error {}

and you can instantiate your custom errors like:

new MyError('message'); // Take a message
new MyError(error); // Take an error
new MyError('message', error); // Take a message and an error

If only such a world existed...!

Library with which You Can Define Custom Errors Easily

Hello world, I've created a library with which you can define custom errors easily!

github.com

As described in TL;DR, you can use it like the following:

const ExtensibleCustomError = require('extensible-custom-error');

class MyError extends ExtensibleCustomError {}

new MyError('message'); // Take a message
new MyError(error); // Take an error
new MyError('message', error); // Take a message and an error

Examples

Let's use it in practice.

The first example is passing an error object to it as its argument.

const ExtensibleCustomError = require('extensible-custom-error');

class MyError extends ExtensibleCustomError {}

function throwBuiltinError() {
  throw new Error('Unknown to Death, Nor known to Life');
}

function wrapErrorWithMyError() {
  try {
    throwBuiltinError();
  } catch (error) {
    throw new MyError(error);
  }
}

function main() {
  try {
    wrapErrorWithMyError();
  } catch (error) {
    console.log(error);
  }
}

main();

By running the code above, you get:

MyError: Error: Unknown to Death, Nor known to Life
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:101:11)
Error: Unknown to Death, Nor known to Life
    at throwBuiltinError (/home/necojackarc/custom_error.js:94:9)
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:99:5)
    at main (/home/necojackarc/custom_error.js:107:5)
    at Object.<anonymous> (/home/necojackarc/custom_error.js:113:1)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)

Cool! The full information of the error has been retained! So helpful!

The next example is to pass both a message and an error:

const ExtensibleCustomError = require('extensible-custom-error');

class MyError extends ExtensibleCustomError {}

function throwBuiltinError() {
  throw new Error('Have withstood Pain to create many Weapons');
}

function wrapErrorWithMyError() {
  try {
    throwBuiltinError();
  } catch (error) {
    throw new MyError('Unlimited Blade Works', error);
  }
}

function main() {
  try {
    wrapErrorWithMyError();
  } catch (error) {
    console.log(error);
  }
}

main();

Then,

MyError: Unlimited Blade Works
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:101:11)
Error: Have withstood Pain to create many Weapons
    at throwBuiltinError (/home/necojackarc/custom_error.js:94:9)
    at wrapErrorWithMyError (/home/necojackarc/custom_error.js:99:5)
    at main (/home/necojackarc/custom_error.js:107:5)
    at Object.<anonymous> (/home/necojackarc/custom_error.js:113:1)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)

New error information will stack on the existing error information prettily!

Summary

Beware you need to write some code to retain error information when you use custom errors with JavaScript. To avoid such pitfalls, I've created necojackarc/extensible-custom-error as an npm module.

Oagariyo!

The original post was published on October 10th, 2018, by me in Japanese.