Skip to content

Conversation

mhofman
Copy link
Member

@mhofman mhofman commented Sep 11, 2025

This PR changes the bespoke "species" check that PromiseResolve does on native promises to lookup whether the __proto__ of the promise matches the .prototype value of the constructor, instead of matching the .constructor value of the promise. If the constructor value is %Promise%, the value used by the spec's abstract operations like Await, no user code executes during PromiseResolve when the value is a native base promise. Closes #3662.

Since Await already adopts a native promise (it does not trigger a .then for those type of values), this change guarantees that no user code is executed when adopting a base native promise (of the same realm), e.g. in async functions when await-ing the result value of a call to another async function.

This PR does not change the way non promise thenables are handled, including when the resolution values are "unexpected thenables" (see thenable curtailment proposal). PromiseResolve may be called with a constructor other than %Promise%, e.g. when the user calls %Promise.resolve% with another receiver, in which case the constructor (receiver) may be able to observe the .prototype lookup.

Effectively for native promises, this moves the trust of the "species" check from the native promise value being tested to the constructor being tested against.

Web compatibility

I cannot imagine a legitimate case where the current .constructor check wouldn't be equivalent to the proposed .__proto__ check. Cases where the check would not be equivalent:

  • Own .constructor property on the promise instance
  • Modified %Promise.prototype%.constructor property
  • Promise with a different prototype than %Promise.prototype%, but that maintains a .constructor of %Promise%.

The latter case is the only remotely legitimate use case. However it is also the reverse case where a value that was previously passed through PromiseResolve would instead become wrapped in a new promise through its .then, continuing to work, albeit with extra ticks.

If needed we could instrument an existing implementation to detect how often a Promise would now be passed through when it previously was wrapped. We could instrument the other direction too, but it doesn't seem worthwhile. If possible. we may also want to measure how often the .constructor check results in user code execution, even if it does result in the same passthrough outcome.

A note on user code execution in these web compat checks

It is possible that looking up .constructor may result in proxy traps triggering, even if the receiver is a native promise, and its __proto__ is %Promise.prototype%: the user code may have replaced the %Promise.prototype%.__proto__ with a proxy, and deleted the original constructor property from %Promise.prototype%.

Promise adoption through resolve functions

This PR previously updated the promise resolution steps of the resolve function to adopt native promises. The scope of this PR has been reduced and the promise adoption part has been spun off in its own proposal.

Comment on lines 49144 to 49177
1. Let _xConstructor_ be ? Get(_x_, *"constructor"*).
1. If SameValue(_xConstructor_, _C_) is *true*, return _x_.
1. Let _xProto_ be ! _x_.[[GetPrototypeOf]]().
1. Let _CPrototype_ be ? Get(_C_, *"prototype"*).
1. If SameValue(_xProto_, _CPrototype_) is *true*, return _x_.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, consider (realPromise => Reflect.apply(Promise.resolve, Object.defineProperty(function(){ throw Error("invoked"); }, "prototype", { value: Promise.prototype }), [realPromise]) === realPromise)(new Promise(() => {})) in an unmodified environment. With the current definition of PromiseResolve, it will invoke the supplied constructor (effectively attempting to return an instance from it), but with the proposed new definition, it will instead return realPromise and the entire expression will be true.

This is basically inverting the loophole that already exists, where instead of a real promise being able to fake association with an arbitrary constructor (and getting the chance to run code in the process), an arbitrary constructor can fake association with any real promise. But since resolve is a method of the constructor anyway, that power totally makes sense. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, .resolve trusts and works in the context of its "constructor" receiver.

@mhofman mhofman changed the title Adopt promises Normative: Adopt promises Sep 11, 2025
@mhofman mhofman marked this pull request as ready for review September 11, 2025 18:27
@mhofman mhofman added normative change Affects behavior required to correctly evaluate some ECMAScript source text needs consensus This needs committee consensus before it can be eligible to be merged. needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 labels Sep 11, 2025
Highlight that %PromiseThenAction% is not accessible to user code when calling HostMakeJobCallback
1. Let _xConstructor_ be ? Get(_x_, *"constructor"*).
1. If SameValue(_xConstructor_, _C_) is *true*, return _x_.
1. Let _xProto_ be ! _x_.[[GetPrototypeOf]]().
1. Let _CPrototype_ be ? Get(_C_, *"prototype"*).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to be really prescriptive about this, we could check if _C_ is a constructor/function. I don't believe the .prototype of a function can ever be an accessor? So we could turn this into ! instead of ?.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some functions don't have a .prototype, which could be installed manually. Also C could be a proxy for a constructor / function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it’s not a constructor, then we can’t new C, so NewPromiseCapability(C) would just throw. I don’t think I we need to consider those.

I’m kinda ambivalent on whether we need to support proxies of the promise constructor. Up to you.

@mhofman
Copy link
Member Author

mhofman commented Sep 12, 2025

The PromiseResolve change addresses #3662.

The resolve steps change implementing promise adoption addresses https://esdiscuss.org/topic/don-t-test-promise-results-on-their-thenability-multiple-times

@mhofman
Copy link
Member Author

mhofman commented Sep 12, 2025

Added details on web compatibility and potential usage counters.

Copy link

The rendered spec for this PR is available at https://tc39.es/ecma262/pr/3689.

@mhofman mhofman changed the title Normative: Adopt promises Normative: PromiseResolve check proto instead of constructor Sep 19, 2025
@mhofman
Copy link
Member Author

mhofman commented Sep 19, 2025

I have updated this PR to reduce the scope to only update PromiseResolve and spun off the promise adoption changes into a proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs consensus This needs committee consensus before it can be eligible to be merged. needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 normative change Affects behavior required to correctly evaluate some ECMAScript source text
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Can we replace the constructor check in PromiseResolve with a prototype check
4 participants