In this blog post, we examine the ECMAScript proposal “Error cause” (by Chengzhong Wu and Hemanth HM). It describes a feature where instances of Error can optionally specify that they were caused by another error.
Sometimes, we catch errors that are thrown during a more deeply nested function call and would like to attach more information to it:
function readFiles(filePaths) {
return filePaths.map(
(filePath) => {
try {
const text = readText(filePath);
const json = JSON.parse(text);
return processJson(json);
} catch (error) {
// (A)
}
});
}
The statements inside the try clause may throw all kinds of errors. In most cases, an error won’t be aware of the path of the file that caused it. That‘s why we would like to attach that information in line A.
The proposal enables us to do the following:
function readFiles(filePaths) {
return filePaths.map(
(filePath) => {
try {
// ···
} catch (error) {
throw new Error(`While processing ${filePath}`, {cause: error});
}
});
}
Error and its subclasses now have an object with options as a second parameter. The first supported option is .cause – the error that caused the current error.
If you subclass Error, it makes sense to support the second parameter with options:
class MyCustomError extends Error {
constructor(message, options) {
super(message, options);
// ···
}
}
.cause AggregateError (created by Promise.any()) If Promise.any() rejects its returned Promise, the rejection value is an instance of AggregateError that records which (zero or more) errors caused the rejection:
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}
AggregateError is a reasonable workaround if .cause is not supported on an engine that you are targeting, however:
AggregateError works best if we are handling multiple concurrent invocations.Error with .cause works best for single non-concurrent calls.The following custom error class supports chaining.
/**
* This subclass of Error supports chaining.
* If available, it uses the built-in support for property `.cause`.
* Otherwise, it sets it up itself.
*
* @see https://github.com/tc39/proposal-error-cause
*/
class CausedError extends Error {
constructor(message, options) {
super(message, options);
if ((isObject(options) && 'cause' in options) && !('cause' in this)) {
const cause = options.cause;
this.cause = cause;
if ('stack' in cause) {
this.stack = this.stack + '\nCAUSE: ' + cause.stack;
}
}
}
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
There are several libraries that support chaining errors. Three examples: