Repository: tc39/ecma262

Issue #2222: Promise.prototype.finally should use onFinally's realm for newly creating functions
Created: 2020-11-03T19:03:58Z
derived from https://bugzilla.mozilla.org/show_bug.cgi?id=1674505#c6
promise.finally(onFinally) and promise.then(onFinally, onFinally) behaves differently in unexpected way if promise and onFinally comes from different realm.
Here's the details.
If Promise.prototype.finally is called with a function, it creates new functions and calls then with them.
https://tc39.es/ecma262/#sec-promise.prototype.finally
26.6.5.3 Promise.prototype.finally ( onFinally )
...
5. If IsCallable(onFinally) is false, then
6. Else,
  a. Let stepsThenFinally be the algorithm steps defined in Then Finally Functions.
  b. Let thenFinally be ! CreateBuiltinFunction(stepsThenFinally, « [[Constructor]], [[OnFinally]] »).
...
7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).

CreateBuiltinFunction uses current realm because realm parameter isn't passed here.
And the current realm is Promise.prototype.finally function's realm.
https://tc39.es/ecma262/#sec-createbuiltinfunction
9.3.3 CreateBuiltinFunction ( steps, internalSlotsList [ , realm [ , prototype ] ] )
...
2. If realm is not present, set realm to the current Realm Record.
...
6. Set func.[[Realm]] to realm.

At this point, handler function passed to Promise.prototype.then comes from different realm for promise.finally(onFinally) and promise.then(onFinally, onFinally).
For promise.finally(onFinally) case, the handler belongs to finally's function's realm.
For promise.then(onFinally, onFinally) case, the handler is onFinally itself.
And this difference is propagated to embedding as following:
Promise.prototype.then calls PerformPromiseThen
https://tc39.es/ecma262/#sec-promise.prototype.then
26.6.5.4 Promise.prototype.then ( onFulfilled, onRejected )
...
5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability).

And PerformPromiseThen calls NewPromiseReactionJob with onFulfilledJobCallback, that contains the onFulfilled in [[Callback]] field.
https://tc39.es/ecma262/#sec-performpromisethen
26.6.5.4.1 PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )
...
3. If IsCallable(onFulfilled) is false, then
4. Else,
  a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
10. Else if promise.[[PromiseState]] is fulfilled, then
  a. Let value be promise.[[PromiseResult]].
  b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value).
  c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).

https://tc39.es/ecma262/#sec-hostmakejobcallback
8.4.2 HostMakeJobCallback ( callback )
...
2. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }.

And NewPromiseReactionJob creates a job with onFulfilled's realm.
https://tc39.es/ecma262/#sec-newpromisereactionjob
26.6.2.1 NewPromiseReactionJob ( reaction, argument )

2. Let handlerRealm be null.
3. If reaction.[[Handler]] is not empty, then
  a. Let getHandlerRealmResult be GetFunctionRealm(reaction.[[Handler]].[[Callback]]).
  b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
  c. Else, set handlerRealm to the current Realm Record.
  d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects.
4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }.

and the realm is passed to HostEnqueuePromiseJob.
It checks if the realm is "fully active" or not.
https://html.spec.whatwg.org/#hostenqueuepromisejob
8.1.5.1 HostEnqueuePromiseJob(job, realm)
1. If realm is not null, then let job settings be the settings object for realm. Otherwise, let job settings be null.
...
6. Queue a microtask on the surrounding agent's event loop to perform the following steps:
  1. If job settings is not null, then check if we can run script with job settings. If this returns "do not run" then return.

https://html.spec.whatwg.org/#check-if-we-can-run-script
The steps to check if we can run script with an environment settings object settings are as follows. They return either "run" or "do not run".
...
1. If the global object specified by settings is a Window object whose Document object is not fully active, then return "do not run".

So, the difference in realm affects here.
If onFinally's global is dying (= not fully active),
promise.finally(onFinally) runs the job, but promise.then(onFinally, onFinally) doesn't run the job.
IIUC, creating new functions in finally is mostly to avoid passing valueOrReason to onFinally,
and using finally's realm isn't much intentional?
If that's the case, it should use onFinally's realm when creating those functions.

Duplicate issue #1077: Missing definitions for "<thing> equivalent to a function that <action>"
Created: 2018-01-26T16:36:02Z
Added in #1073

Closing comment (2020-11-24T23:15:47Z): Related to #2222
if the function is passed to Promise.prototype.then ("Then Finally Function"'s case), the function's [[Realm]] slot affects embedding behavior,
so it should be explicit about [[Realm]] slot.