ECMAScript proposal: Error cause (chaining errors)

[2021-06-15] dev, javascript, es proposal
(Ad, please don’t block)

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.

Why would we want to chain errors?  

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.

How to chain errors  

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.

Consequence for your own code  

If you subclass Error, it makes sense to support the second parameter with options:

class MyCustomError extends Error {
  constructor(message, options) {
    super(message, options);
    // ···
  }
}

Alternatives to the built-in support for .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.

A custom error class  

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';
}

A library  

There are several libraries that support chaining errors. Three examples:

More information on exception handling in JavaScript