Filtered Promise#catch
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.
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
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:
- If
matcher
is a function, check ifmatcher.prototype instanceof Error
, if so, useinstanceof
to check if a rejected error matches - If
matcher
is a function that doesn't inherit fromError
, execute it with the rejected error to get the match condition - 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
@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).
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.
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
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
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.
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
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.
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
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!
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.
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
orerror
) could be used to instead support the same functionality.Terminology:
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 matchingIn 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;