necojackarc’s blog

A software engineer's blog who works for a web service company

Decent Use of JavaScript Custom Errors

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

In the JavaScript world, pitfalls are everywhere; one of them seems to be error handling.

This post shows one way to use JavaScript's custom errors decently, which isn't utilized so often.

TL;DR

necojackarc/extensible-custom-error enables you to define handy custom errors easily, which can also take an error object as its argument!

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 helpful!

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)

Touch on JavaScrript Error Handling

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

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

This code outputs Caught a Type Error.

You can do the same 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 also outputs Caught a TypeError. With async/await, you can use regular try/catch, so I don't show examples here.

Define Custom Errors

Let's define custom errors and branch processing with instanceof like with built-in errors.

Pitfall 1: Data gets lost with a simple inheritance

You can't implement JavaScript's custom errors perfectly just by extending Error.

// 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 get:

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 and branch processing is succeeded, but the type of MyError got lost.

This can be avoided by overwriting the name property with constructor, or with Error.captureStackTrace available only 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 can 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's working well.

This is also explained in the MDN page.

Use Custom Errors

Use the custom errors defined so far.

Pitfall 2: Constructors of errors only take a message

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

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

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

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

When you run them, 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 the first glance, but you've lost some stack traces. In fact, you got the same results as you passed error.toString().

An output of error.toStirng() is an error type and a message, so it'll be Error: Built-in error. In brief, stack traces are ignored even though you pass an error object.

To cope with that, you need to add some logic to merge a given stack traces to ones generated here to a constructor.

With JavaScript built-in errors, of course, you can't pass a message and an error like throw new myError('message', error), which you can with like Java.

Summary of Pitfalls around Custom

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

Ideal World

You can define your own errors with:

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 about 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!