Filtered Promise#catch

# Peter Jaszkowiak (7 years ago)

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:

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

Which is a shortcut for the following:

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

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:

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:

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

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;
# Jonathan Barronville (7 years ago)

Yes, +1. Filtered .catch(…) is a Bluebird feature that I use quite a lot. Native support would be great. I've personally only ever needed to use the constructor matcher version (i.e., the version which uses instanceof under the hood), but I can see why support for the other versions (object predicate and function predicate) could be useful.

# Christopher Thorn (7 years ago)

I agree that catch guards are useful, but they can be implemented in userland fairly ergonomically:

function guard(predicate, callback) {
  return function guarded(reason) {
    if (!predicate(reason)) {
      throw reason;
    }
    return callback(reason);
  };
}

function instanceOf(constructor, callback) {
  return guard(reason => reason instanceof constructor, callback);
}

Promise.resolve('invalid')
  .then(JSON.parse)
  .catch(instanceOf(SyntaxError, reason => {
    // do something to handle the syntax error, or perhaps just silence it
  }));

, Chris

# Peter Jaszkowiak (7 years ago)

True, it's a fairly trivial thing, but so is Array.prototype.includes. I can give a hundred other examples.

There is a reason that so many people use underscore, ramda, lodash, etc: the "standard library" in Javascript is incomplete. We should aim to provide as many of these helpful utilities as possible. Not only does it improve developer experience, it also reduces payloads people need to download over the wire.

I can understand why people are hesitant to add syntactic additions to JavaScript, but when it comes to such useful cases that are already in wide use via libraries, I can't understand the reservations.

You did provide a good method name (guard) if the final decision is to avoid overloading catch (which I don't mind either way, in some ways it would be preferable so the argument order could be reversed).

Anyways, I think the best implementation would do three things:

  1. If matcher is a function, check if matcher.prototype instanceof Error, if so, use instanceof to check if a rejected error matches
  2. If matcher is a function that doesn't inherit from Error, execute it with the rejected error to get the match condition
  3. If matcher is an object, check every own enumerable property against the rejected error to get the match condition

This seems pretty complicated, in that it doesn't seem to match the principle of avoiding overloading in the standard library. I think in this case, removing any of this functionality is a significant loss, but if any were to be removed, I'd choose #3.

, Peter

# Jonathan Barronville (7 years ago)

@Christopher Thorn: I do see your point, but as pointed out by Peter Jaszkowiak, there are already several little facilities which have been added to JavaScript that are actually useful but could relatively easily/straightforwardly be implemented in userland. This particular feature (i.e., filtered catch) is a very useful one that I, like Peter Jaszkowiak and likely lots of other folks, find myself using a lot. In fact, there are at least a couple projects where I've wanted to use Bluebird just due to the usefulness of that feature alone and how clean it allows my code to be. Of course, we don't want to add every potentially useful feature to the language, but I think a case like this is a perfect example of when it makes sense (IMHO).

# Michał Wadas (7 years ago)

I don't get why argument "it's easy to implement in user land" is raised.

All Array methods are easy to implement in user land. Including recent additions like .includes and ES2015 methods.

Promises are easy to implement in user land. Smallest implementation is less than 1kB AFAIR.

Maps, Sets, WeakSets are trivial to implement in user land if WeakMap is present. Probably less than 50 lines of code.

Object.values, Object.entries, Object.assign, Object.getOwnPropertyDescriptors, Object.is, Array.from, Array.of. These methods were added recently but they can be implemented in user land.

# T.J. Crowder (7 years ago)

On Wed, Oct 11, 2017 at 1:28 PM, Michał Wadas <michalwadas at gmail.com> wrote:

I don't get why argument "it's easy to implement in user land" is raised. (etc)

Indeed (with the exception of promises, which couldn't be properly implemented in userland without environment-specific facilities for doing something asynchronously).

Which brings us back to esdiscuss.org/topic/guidelines-and-general-direction-for-standard-library-proposals (esdiscuss.org likes to munge that link; here's a smaller version: tinyurl.com/ydyu9xtt): Until or unless TC39 thrash out a consensus (I'm told there is none at present) about whether a robust or minimal standard library is the direction to go in future, additions to it will tend to be fairly arbitrary, which isn't a great place to be.

-- T.J. Crowder

# Christopher Thorn (7 years ago)

I don't think it's pedantic to point out that you're misstating my argument. I didn't say "easy", I said "ergonomic", after all.

I've used bluebird quite a lot, and I've also used native promises and other promise libraries without filtered catch. At first, I missed this feature from bluebird, but I no longer do.

Personally, I prefer the ergonomics of helper functions like I demonstrated to an overloaded catch method, especially one which does different things depending on the types of the arguments that are passed (one behavior for the instanceof check, another for a function predicate and another for an object predicate). Using an separate function gives the user a chance to specify exactly the behavior they want, no more and no less, and gives the user a chance to name the filter.

, Chris

# Peter Jaszkowiak (7 years ago)

If you're going to distinguish between "ergonomic" and "easy" in the context of JavaScript development, you're going to have to define what you think the difference is.

For something that should, imo, be used in almost every case that catch is used, I don't think that it just being ergonomic to create your own helper functions is enough. Are you going to create your own helper function like that for every error type across every file?

Do you want the developer to provide their own guard etc helper functions like you did? Why should they have to?

As I said before, I think the criticism of overloading is fine. However, this is something more useful than Array#includes and already in use more often than that Array method.

We can use different method names to separate different types of guards if you feel like it's a preferable idea. However, it definitely needs to be done because there is significant demand demonstrated by many people still using bluebird for these utility functions.

# Christopher Thorn (7 years ago)

Inline

If you're going to distinguish between "ergonomic" and "easy" in the context of JavaScript development, you're going to have to define what you think the difference is.

The difference is that it seemed like people thought I meant "this is easy to implement", but I was talking about the ergonomics of using the functions once they're defined. Or trying to, at least :)

For something that should, imo, be used in almost every case that catch is used, I don't think that it just being ergonomic to create your own helper functions is enough. Are you going to create your own helper function like that for every error type across every file?

In the codebase I am currently working on, I have functions like these defined in modules, which I import where they are needed.

Do you want the developer to provide their own guard etc helper functions like you did? Why should they have to?

They could be defined by a library, but they don't need to be methods the Promise prototype.

I prefer separate functions because of their explicit intent. In particular, I'm not a huge fan of bluebird's overloaded catch method. However, if you don't overload it, it seems like you'd need to specify three methods in addition to catch: catchInstancesOf, one for function predicates, and one for object predicates (I'm not sure what I'd call those two). Having four methods for handling rejections is too many.

Maybe functions like the ones I demonstrated could be added to the standard library (say, as static properties of the Promise object).

, Chris

# Jordan Harband (7 years ago)

Here's why catch guards aren't an easy feature: Promise catch mirrors syntactic catch. Promise .catch will not get any new features that aren't simultaneously available to syntactic catch - full stop.

OK, now you're adding syntax, great! How do you match types? instanceof is utterly unreliable, because it doesn't work cross-realm, so a number of us on the committee would block on that.

OK, now you're adding a cross-realm brand-checking mechanism. There's committee members who would block on that.

OK, maybe now you're adding a generic matching protocol, because what if you want users to be able to supply more complex guard checks - oh wait, there's already a proposal for that: tc39/proposal-pattern-matching

Please do not assume that any given fun idea, given all the edge cases, constraints, and interoperability requirements, will be "easy" to add.

# doodad-js Admin (7 years ago)

OK, now you're adding syntax, great! How do you match types? instanceof is utterly unreliable, because it doesn't work cross-realm, so a number of us on the committee would block on that.

OK, now you're adding a cross-realm brand-checking mechanism. There's committee members who would block on that.

I’ve fixed that (cross-realms) with GUUIDs and a decent type subsystem.

From: Jordan Harband [mailto:ljharb at gmail.com] Sent: Wednesday, October 11, 2017 4:06 PM To: Christopher Thorn <morphcham at gmail.com>

Cc: es-discuss <es-discuss at mozilla.org>

Subject: Re: Re: Filtered Promise#catch

Here's why catch guards aren't an easy feature: Promise catch mirrors syntactic catch. Promise .catch will not get any new features that aren't simultaneously available to syntactic catch - full stop.

OK, now you're adding syntax, great! How do you match types? instanceof is utterly unreliable, because it doesn't work cross-realm, so a number of us on the committee would block on that.

OK, now you're adding a cross-realm brand-checking mechanism. There's committee members who would block on that.

OK, maybe now you're adding a generic matching protocol, because what if you want users to be able to supply more complex guard checks - oh wait, there's already a proposal for that: tc39/proposal-pattern-matching

Please do not assume that any given fun idea, given all the edge cases, constraints, and interoperability requirements, will be "easy" to add.

On Wed, Oct 11, 2017 at 7:52 AM, Christopher Thorn <morphcham at gmail.com <mailto:morphcham at gmail.com> > wrote:

Inline

If you're going to distinguish between "ergonomic" and "easy" in the context of JavaScript development, you're going to have to define what you think the difference is.

The difference is that it seemed like people thought I meant "this is easy to implement", but I was talking about the ergonomics of using the functions once they're defined. Or trying to, at least :)

For something that should, imo, be used in almost every case that catch is used, I don't think that it just being ergonomic to create your own helper functions is enough. Are you going to create your own helper function like that for every error type across every file?

In the codebase I am currently working on, I have functions like these defined in modules, which I import where they are needed.

Do you want the developer to provide their own guard etc helper functions like you did? Why should they have to?

They could be defined by a library, but they don't need to be methods the Promise prototype.

I prefer separate functions because of their explicit intent. In particular, I'm not a huge fan of bluebird's overloaded catch method. However, if you don't overload it, it seems like you'd need to specify three methods in addition to catch: catchInstancesOf, one for function predicates, and one for object predicates (I'm not sure what I'd call those two). Having four methods for handling rejections is too many.

Maybe functions like the ones I demonstrated could be added to the standard library (say, as static properties of the Promise object).

, Chris

# Jordan Harband (7 years ago)

GUUIDs only work if they're deterministically generate-able in each realm; which isn't something that typically can be performantly and robustly done. If you've got such a solution, please ping me directly (no need to pollute the list) as I'd love to look into it!

# Michał Wadas (7 years ago)

It's possible to standardize already existing filtered catch from SpiderMonkey.

try { throw new Error('42'); } catch(e if e.message === 42) {

}

Then Promise.prototype.catch extension can work in very similar manner - by predicate function and user code can easily adopt pattern-matching in future.