Filtered Promise#catch

Peter Jaszkowiak p.jaszkow at gmail.com
Tue Oct 10 23:06:28 UTC 2017


Excuse me if this has been discussed previously, I did try to find existing
discussions.

Bluebird has very useful functionality in `Promise.prototype.catch`, which
allows for filtering certain error types. Here is an example:

```js
database.get('user:Bob')
  .catch(UserNotFoundError, (err) => {
    console.error('User not found: ', err);
  })
```

Which is a shortcut for the following:

```js
database.get('user:Bob')
  .catch((err) => {
    if (err instanceof UserNotFoundError) {
      console.error('User not found: ', err);
      return;
    }

    throw err;
  })
```

I think this would be a huge improvement to error handling in asynchronous
situations, especially since many people dislike using `try { ... } catch
(e) { ... }` syntax (which would also benefit from some sort of error
filtering).

### Options

I think that passing in the matching argument as the second argument is
preferable to passing it as the first argument, but in the spririt of
compatibility, supporting it in the Bluebird fashion is best.
Alternatively, a new prototype method (like `catchFilter` or `error`) could
be used to instead support the same functionality.

Terminology:
- Callback
  The function passed as the next operation in the chain, to be called if a
rejection occurs
- Matcher
  The argument passed to select a certain type of error to be caught, and
then execute the provided callback

#### Use `instanceof`
The class or constructor would be passed as the matcher

This on its own is  It would not support instances from other contexts, nor
would it support custom errors created without subclassing (like manually
setting error.name).

Example
```js
const err = new CustomError();
Promise.reject(err)
  .catch(CustomError, (customError) => {
    // handle CustomError s
  })
  .catch((otherError) => {
    // handle other errors
  });
```

#### Use pattern matching
An object would be passed as the matcher, and it's own enumerable
properties would be compared with properties of the error instance. If all
properties on the given matcher are strictly equal to the same properties
on the error instance, it's a match.

Example:
```js
const err = new Error();
err.name = 'CustomError';
Promise.reject(err)
  .catch({ name: 'CustomError' }, (customError) => {
    // handle CustomError s
  })
  .catch((otherError) => {
    // handle other errors
  });
```

This would allow for matching any error type, and could support a subset of
the `instanceof` check by doing something like `{ constructor: CustomError
}`.

#### Use a matcher function
A function would be passed as the matcher, receiving the err as its
argument. It would then be able to do any operation on the error instance
to check if it is the correct error type.

Example:
```js
const err = new Error();
err.name = 'CustomError';
Promise.reject(err)
  .catch(err => (err.name === 'CustomError'), (customError) => {
    // handle CustomError s
  })
  .catch((otherError) => {
    // handle other errors
  });
```

However, this is almost no different from just including the tests in the
catch body itself. It's verbose enough that the benefit of supporting this
is not nearly as significant than the other options.

#### Support `instanceof` and pattern matching
In my opinion, this is the best of both worlds. You get to support the
basic `instanceof` case with inheritance, etc, while also supporting
matching more custom errors made in a less standard manner.

The method would check if the matcher is a function, and if so, it would
use `instanceof`. Otherwise, it would treat the argument as an object and
compare the properties.


### Example Naive Polyfill

```js
const origCatch = Promise.prototype.catch;
Promise.prototype.catch = { catch (ErrorType, callback) {
  if (typeof callback !== 'function' && typeof ErrorType === 'function') {
    callback = ErrorType;
    ErrorType = null;
  }

  if (!ErrorType || !(typeof ErrorType === 'object' || typeof ErrorType ===
'function')) {
    return origCatch.call(this, callback);
  }

  // if the ErrorType is a function, use instanceof
  if (typeof ErrorType === 'function') {
    return origCatch.call(this, (err) => {
      if (err instanceof ErrorType) {
        return callback(err);
      }

      throw err;
    });
  }

  // otherwise use pattern matching
  return origCatch.call(this, (err) => {
    const matches = Object.entries(ErrorType)
      .every(([key, value]) => (err[key] === value));
    if (matches) {
      return callback(err);
    }

    throw err;
  });
} }.catch;
```
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20171010/b109cf67/attachment-0001.html>


More information about the es-discuss mailing list