Differences between Promise.prototype methods with regard to what constitutes what constitutes compatible receiver
This is intentional - catch
delegates to then
, so that a subclass that
overwrites then
doesn't have to also override catch
(and the same for
finally
, which also calls into then
).
This is intentional - `catch` delegates to `then`, so that a subclass that overwrites `then` doesn't have to also override `catch` (and the same for `finally`, which also calls into `then`). On Fri, Jul 20, 2018 at 4:29 AM, Darien Valentine <valentinium at gmail.com> wrote: > In `Promise.prototype.then`: > > > 1. Let promise be the this value. > > 2. If IsPromise(promise) is false, throw a TypeError exception. > > [ ... ] > > In `Promise.prototype.finally`: > > > 1. Let promise be the this value. > > 2. If Type(promise) is not Object, throw a TypeError exception. > > [...] > > In `Promise.prototype.catch`: > > > 1. Let promise be the this value. > > 2. Return ? Invoke(promise, "then", « undefined, onRejected »). > > First, this means that only `then` requires the this value to be a Promise: > > ```js > for (const key of [ 'then', 'finally', 'catch' ]) { > try { > Promise.prototype[key].call({ > then: () => console.log(`${ key } doesn’t brand check its this > value`) > }); > } catch (err) { > console.log(`${ key } does brand check its this value`); > } > } > > // > then does brand check its this value > // > finally doesn’t brand check its this value > // > catch doesn’t brand check its this value > ``` > > Second, note that `Invoke` uses `GetV`, not `Get`. Thus: > > ```js > for (const key of [ 'then', 'finally', 'catch' ]) { > try { > String.prototype.then = () => > console.log(`${ key } casts this value to object`); > > Promise.prototype[key].call('foo'); > } catch (err) { > console.log(`${ key } doesn’t cast this value to object`); > } > } > > // > then doesn’t cast this value to object > // > finally doesn’t cast this value to object > // > catch casts this value to object > ``` > > On reflection, I think I see the logic to this: > > - `Promise.prototype.then` ends up executing `PerformPromiseThen`, which > requires its first argument to be a native promise object. > - `Promise.prototype.finally` ends up executing `SpeciesConstructor`, > which requires its first argument to be an object. > - `Promise.prototype.catch` does neither. > > However the inconsistency within this trio seems pretty odd to me. I > suppose I would have expected them all to be as constrained as the most > constrained method needed to be for the sake of uniformity, given that they > constitute a single API. Conversely, if the goal was for each method to be > exactly as lenient as is possible, then `finally` seems to be > over-constrained; it seems like `C` could have just defaulted to `Promise` > in cases where `SpeciesConstructor` wasn’t applicable, making it as lenient > as `catch`. > > I wasn’t able to find prior discussion about this, though it’s a bit hard > to search for, so I may be missing it. Do these behaviors seem odd to > anyone else, or is it what you’d expect? > > _______________________________________________ > es-discuss mailing list > es-discuss at mozilla.org > https://mail.mozilla.org/listinfo/es-discuss > > -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180720/4e6c8c48/attachment.html>
That makes sense for sure, but I don’t think subclassing is impacted by receiver constraints in this regard either way, since subclasses will still be “IsPromise” promises. What is affected is portability of the methods to “non-IsPromise” thenables. Catch not requiring an IsPromise receiver makes sense from that angle:
OffbrandPromise.prototype.catch = Promise.prototype.catch;
But if portability to non-native, non-Promise-subclass thenables is the goal, I’d still have expected finally to be the same “level” of generic as catch. Neither require their receiver to be an IsPromise-promise, but catch doesn’t even require its receiver to be an object, while finally does. This is a very minor thing obviously, but I wondered if it might still be web safe at this point to make catch require its receiver to be an object like finally does. It would be more consistent, and it’s pretty hard to imagine the current ability to call catch on a primitive, as in the second example above, being a useful behavior.
That makes sense for sure, but I don’t think subclassing is impacted by receiver constraints in this regard either way, since subclasses will still be “IsPromise” promises. What is affected is portability of the methods to “non-IsPromise” thenables. Catch not requiring an IsPromise receiver makes sense from that angle: ```js OffbrandPromise.prototype.catch = Promise.prototype.catch; ``` But if portability to non-native, non-Promise-subclass thenables is the goal, I’d still have expected finally to be the same “level” of generic as catch. Neither require their receiver to be an IsPromise-promise, but catch doesn’t even require its receiver to be an object, while finally does. This is a very minor thing obviously, but I wondered if it might still be web safe at this point to make catch require its receiver to be an object like finally does. It would be more consistent, and it’s pretty hard to imagine the current ability to call catch on a primitive, as in the second example above, being a useful behavior. On Fri, Jul 20, 2018 at 7:10 PM Jordan Harband <ljharb at gmail.com> wrote: > This is intentional - `catch` delegates to `then`, so that a subclass that > overwrites `then` doesn't have to also override `catch` (and the same for > `finally`, which also calls into `then`). > > On Fri, Jul 20, 2018 at 4:29 AM, Darien Valentine <valentinium at gmail.com> > wrote: > >> In `Promise.prototype.then`: >> >> > 1. Let promise be the this value. >> > 2. If IsPromise(promise) is false, throw a TypeError exception. >> > [ ... ] >> >> In `Promise.prototype.finally`: >> >> > 1. Let promise be the this value. >> > 2. If Type(promise) is not Object, throw a TypeError exception. >> > [...] >> >> In `Promise.prototype.catch`: >> >> > 1. Let promise be the this value. >> > 2. Return ? Invoke(promise, "then", « undefined, onRejected »). >> >> First, this means that only `then` requires the this value to be a >> Promise: >> >> ```js >> for (const key of [ 'then', 'finally', 'catch' ]) { >> try { >> Promise.prototype[key].call({ >> then: () => console.log(`${ key } doesn’t brand check its this >> value`) >> }); >> } catch (err) { >> console.log(`${ key } does brand check its this value`); >> } >> } >> >> // > then does brand check its this value >> // > finally doesn’t brand check its this value >> // > catch doesn’t brand check its this value >> ``` >> >> Second, note that `Invoke` uses `GetV`, not `Get`. Thus: >> >> ```js >> for (const key of [ 'then', 'finally', 'catch' ]) { >> try { >> String.prototype.then = () => >> console.log(`${ key } casts this value to object`); >> >> Promise.prototype[key].call('foo'); >> } catch (err) { >> console.log(`${ key } doesn’t cast this value to object`); >> } >> } >> >> // > then doesn’t cast this value to object >> // > finally doesn’t cast this value to object >> // > catch casts this value to object >> ``` >> >> On reflection, I think I see the logic to this: >> >> - `Promise.prototype.then` ends up executing `PerformPromiseThen`, which >> requires its first argument to be a native promise object. >> - `Promise.prototype.finally` ends up executing `SpeciesConstructor`, >> which requires its first argument to be an object. >> - `Promise.prototype.catch` does neither. >> >> However the inconsistency within this trio seems pretty odd to me. I >> suppose I would have expected them all to be as constrained as the most >> constrained method needed to be for the sake of uniformity, given that they >> constitute a single API. Conversely, if the goal was for each method to be >> exactly as lenient as is possible, then `finally` seems to be >> over-constrained; it seems like `C` could have just defaulted to `Promise` >> in cases where `SpeciesConstructor` wasn’t applicable, making it as lenient >> as `catch`. >> >> I wasn’t able to find prior discussion about this, though it’s a bit hard >> to search for, so I may be missing it. Do these behaviors seem odd to >> anyone else, or is it what you’d expect? >> >> _______________________________________________ >> es-discuss mailing list >> es-discuss at mozilla.org >> https://mail.mozilla.org/listinfo/es-discuss >> >> > -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180721/8063c474/attachment.html>
That seems reasonable, although I'm not sure of the value of that
consistency. It'd have to be proven to be web-compatible - ie, if anyone
was adding a .then
to a primitive prototype, they might be relying on the
current behavior of .catch
.
That seems reasonable, although I'm not sure of the value of that consistency. It'd have to be proven to be web-compatible - ie, if anyone was adding a `.then` to a primitive prototype, they might be relying on the current behavior of `.catch`. On Fri, Jul 20, 2018 at 9:58 PM, Darien Valentine <valentinium at gmail.com> wrote: > That makes sense for sure, but I don’t think subclassing is impacted by > receiver constraints in this regard either way, since subclasses will still > be “IsPromise” promises. What is affected is portability of the methods to > “non-IsPromise” thenables. Catch not requiring an IsPromise receiver makes > sense from that angle: > > ```js > OffbrandPromise.prototype.catch = Promise.prototype.catch; > ``` > > But if portability to non-native, non-Promise-subclass thenables is the > goal, I’d still have expected finally to be the same “level” of generic as > catch. Neither require their receiver to be an IsPromise-promise, but catch > doesn’t even require its receiver to be an object, while finally does. This > is a very minor thing obviously, but I wondered if it might still be web > safe at this point to make catch require its receiver to be an object like > finally does. It would be more consistent, and it’s pretty hard to imagine > the current ability to call catch on a primitive, as in the second example > above, being a useful behavior. > > On Fri, Jul 20, 2018 at 7:10 PM Jordan Harband <ljharb at gmail.com> wrote: > >> This is intentional - `catch` delegates to `then`, so that a subclass >> that overwrites `then` doesn't have to also override `catch` (and the same >> for `finally`, which also calls into `then`). >> >> On Fri, Jul 20, 2018 at 4:29 AM, Darien Valentine <valentinium at gmail.com> >> wrote: >> >>> In `Promise.prototype.then`: >>> >>> > 1. Let promise be the this value. >>> > 2. If IsPromise(promise) is false, throw a TypeError exception. >>> > [ ... ] >>> >>> In `Promise.prototype.finally`: >>> >>> > 1. Let promise be the this value. >>> > 2. If Type(promise) is not Object, throw a TypeError exception. >>> > [...] >>> >>> In `Promise.prototype.catch`: >>> >>> > 1. Let promise be the this value. >>> > 2. Return ? Invoke(promise, "then", « undefined, onRejected »). >>> >>> First, this means that only `then` requires the this value to be a >>> Promise: >>> >>> ```js >>> for (const key of [ 'then', 'finally', 'catch' ]) { >>> try { >>> Promise.prototype[key].call({ >>> then: () => console.log(`${ key } doesn’t brand check its this >>> value`) >>> }); >>> } catch (err) { >>> console.log(`${ key } does brand check its this value`); >>> } >>> } >>> >>> // > then does brand check its this value >>> // > finally doesn’t brand check its this value >>> // > catch doesn’t brand check its this value >>> ``` >>> >>> Second, note that `Invoke` uses `GetV`, not `Get`. Thus: >>> >>> ```js >>> for (const key of [ 'then', 'finally', 'catch' ]) { >>> try { >>> String.prototype.then = () => >>> console.log(`${ key } casts this value to object`); >>> >>> Promise.prototype[key].call('foo'); >>> } catch (err) { >>> console.log(`${ key } doesn’t cast this value to object`); >>> } >>> } >>> >>> // > then doesn’t cast this value to object >>> // > finally doesn’t cast this value to object >>> // > catch casts this value to object >>> ``` >>> >>> On reflection, I think I see the logic to this: >>> >>> - `Promise.prototype.then` ends up executing `PerformPromiseThen`, which >>> requires its first argument to be a native promise object. >>> - `Promise.prototype.finally` ends up executing `SpeciesConstructor`, >>> which requires its first argument to be an object. >>> - `Promise.prototype.catch` does neither. >>> >>> However the inconsistency within this trio seems pretty odd to me. I >>> suppose I would have expected them all to be as constrained as the most >>> constrained method needed to be for the sake of uniformity, given that they >>> constitute a single API. Conversely, if the goal was for each method to be >>> exactly as lenient as is possible, then `finally` seems to be >>> over-constrained; it seems like `C` could have just defaulted to `Promise` >>> in cases where `SpeciesConstructor` wasn’t applicable, making it as lenient >>> as `catch`. >>> >>> I wasn’t able to find prior discussion about this, though it’s a bit >>> hard to search for, so I may be missing it. Do these behaviors seem odd to >>> anyone else, or is it what you’d expect? >>> >>> _______________________________________________ >>> es-discuss mailing list >>> es-discuss at mozilla.org >>> https://mail.mozilla.org/listinfo/es-discuss >>> >>> >> -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180721/317dc641/attachment-0001.html>
In
Promise.prototype.then
:In
Promise.prototype.finally
:In
Promise.prototype.catch
:First, this means that only
then
requires the this value to be a Promise:for (const key of [ 'then', 'finally', 'catch' ]) { try { Promise.prototype[key].call({ then: () => console.log(`${ key } doesn’t brand check its this value`) }); } catch (err) { console.log(`${ key } does brand check its this value`); } } // > then does brand check its this value // > finally doesn’t brand check its this value // > catch doesn’t brand check its this value
Second, note that
Invoke
usesGetV
, notGet
. Thus:for (const key of [ 'then', 'finally', 'catch' ]) { try { String.prototype.then = () => console.log(`${ key } casts this value to object`); Promise.prototype[key].call('foo'); } catch (err) { console.log(`${ key } doesn’t cast this value to object`); } } // > then doesn’t cast this value to object // > finally doesn’t cast this value to object // > catch casts this value to object
On reflection, I think I see the logic to this:
Promise.prototype.then
ends up executingPerformPromiseThen
, which requires its first argument to be a native promise object.Promise.prototype.finally
ends up executingSpeciesConstructor
, which requires its first argument to be an object.Promise.prototype.catch
does neither.However the inconsistency within this trio seems pretty odd to me. I suppose I would have expected them all to be as constrained as the most constrained method needed to be for the sake of uniformity, given that they constitute a single API. Conversely, if the goal was for each method to be exactly as lenient as is possible, then
finally
seems to be over-constrained; it seems likeC
could have just defaulted toPromise
in cases whereSpeciesConstructor
wasn’t applicable, making it as lenient ascatch
.I wasn’t able to find prior discussion about this, though it’s a bit hard to search for, so I may be missing it. Do these behaviors seem odd to anyone else, or is it what you’d expect?