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