BFE.dev solution for JavaScript Quiz
4. Promise then callbacks II

TL;DR

  1. then() creates a new Promise, in which the return value of the callbacks are used in resolve() or reject() internally
    1. if a new Promise is returned, it will be chained.
  2. Promise.prototype.finally() returns a new Promise, if the passed callback throws an error or returns a rejected promise, the promise returned by finally() will be rejected with that value instead, therwise the return value of the handler does NOT affect the state of the original promise.
Promise.resolve(1)
  .then((val) => {
    console.log(val) // 1
    return val + 1
  })
  // since 2 is returned in above callback
  // Promise of this then() is fulfilled with value: 2
  .then((val) => {
    console.log(val) // 2
  })
  // since nothing is returned in above callback
  // Promise of this then() is fulfilled with value: undefined
  .then((val) => {
    console.log(val) // undefined
    return Promise.resolve(3).then((val) => {
      console.log(val) // 3
    })
    // since nothing is returned in above callback
    // Promise of this then() is fulfilled with value: undefined
  })
  // since a Promise is returned and it is fulfilled with value: undefined
  // Promise of this then() is fulfilled with value: undefined

  .then((val) => {
    console.log(val) // undefined
    return Promise.reject(4)
  })
  // since a rejected Promise with value 4 is returned
  // Promise of this then() is rejected with value: 4
  .catch((val) => {
    console.log(val) // 4
  })
  // since above callback returns undefined
  // Promise of this catch() is fulfilled with value: undefined
  .finally((val) => {
    console.log(val) // undefined
    return 10
  })
  // finally() returns a Promise, though 10 is returned in above callback, but it doesn't affect the original promise
  // so Promise of this finally() is fulfilled with value: undefined
  .then((val) => {
    console.log(val) // undefined
  })

Below is a longer version of the explanation based on ECMAScript Spec.

Promise.prototype.then()

From ECMAScript Spec, it does below things.

  1. Let promise be the this value.
  2. If IsPromise(promise) is false, throw a TypeError exception.
  3. Let C be ? SpeciesConstructor(promise, %Promise%).
  4. Let resultCapability be ? NewPromiseCapability(C).
  5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability).

Basically it says it creates a new Promise object, and more steps are in below function.

PerformPromiseThen()

According to ECMAScript Spec, Promise.prototype.then() works as below.

  1. Assert: IsPromise(promise) is true.
  2. If resultCapability is not present, then
    1. Set resultCapability to undefined.
  3. If IsCallable(onFulfilled) is false, then
    1. Let onFulfilledJobCallback be empty.
  4. Else,
    1. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
  5. If IsCallable(onRejected) is false, then
    1. Let onRejectedJobCallback be empty.
  6. Else,
    1. Let onRejectedJobCallback be HostMakeJobCallback(onRejected).
  7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
  8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }.
  9. If promise.[[PromiseState]] is pending, then
    1. Append fulfillReaction to promise.[[PromiseFulfillReactions]].
    2. Append rejectReaction to promise.[[PromiseRejectReactions]].
  10. Else if promise.[[PromiseState]] is fulfilled, then
    1. Let value be promise.[[PromiseResult]].
    2. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value).
    3. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
  11. Else,
    1. Assert: The value of promise.[[PromiseState]] is rejected.
    2. Let reason be promise.[[PromiseResult]].
    3. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
    4. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
    5. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]).
  12. Set promise.[[PromiseIsHandled]] to true.
  13. If resultCapability is undefined, then
    1. Return undefined.
  14. Else,
    1. Return resultCapability.[[Promise]].

Basically it says

  1. create callbacks(fulfillReaction, rejectReaction) for fulfilment and rejection (7 and 8)
  2. register callbacks if the promise is pending (9)
  3. trigger the callbacks if promise is fulfilled or rejected (10 and 11)

Notice that in step 3 and 5, it is checking if argument is function or not (IsCallable). If not function, the internal of fulfillReaction/rejectReaction is empty

What happens in _empty_ is defined in NewPromiseReactionJob(), which is how the fulfillReaction and rejectReaction are executed.

NewPromiseReactionJob()

According to ECMAScript Spec,

  1. Let _job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called:
    1. Let promiseCapability be reaction.[[Capability]].
    2. Let type be reaction.[[Type]].
    3. Let handler be reaction.[[Handler]].
    4. If handler is empty, then
      1. If type is Fulfill, let handlerResult be NormalCompletion(argument).
      2. Else,
        1. Assert: type is Reject.
        2. Let handlerResult be ThrowCompletion(argument).
    5. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
    6. If promiseCapability is undefined, then
      1. Assert: handlerResult is not an abrupt completion.
      2. Return empty.
    7. Assert: promiseCapability is a PromiseCapability Record.
    8. If handlerResult is an abrupt completion, then
      1. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
    9. Else,
      1. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
  2. Let handlerRealm be null.
  3. If reaction.[[Handler]] is not empty, then
    1. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])).
    2. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
    3. Else, set handlerRealm to the current Realm Record.
    4. 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 }.

Let's focus on step 4 & 5, it says

  1. step 4: if callback (handler) is empty, then the existing fulfillment value (argument) is used
  2. step 5: otherwise, the return value of callback (handler) is used.

After this the value is used to resolve() or reject() the promise object.

resolve()

According to ECMAScript spec,

  1. Let F be the active function object.
  2. Assert: F has a [[Promise]] internal slot whose value is an Object.
  3. Let promise be F.[[Promise]].
  4. Let alreadyResolved be F.[[AlreadyResolved]].
  5. If alreadyResolved.[[Value]] is true, return undefined.
  6. Set alreadyResolved.[[Value]] to true.
  7. If SameValue(resolution, promise) is true, then
    1. Let selfResolutionError be a newly created TypeError object.
    2. Perform RejectPromise(promise, selfResolutionError).
    3. Return undefined.
  8. If resolution is not an Object, then
    1. Perform FulfillPromise(promise, resolution).
    2. Return undefined.
  9. Let then be Completion(Get(resolution, "then")).
  10. If then is an abrupt completion, then
    1. Perform RejectPromise(promise, then.[[Value]]).
    2. Return undefined.
  11. Let thenAction be then.[[Value]].
  12. If IsCallable(thenAction) is false, then
    1. Perform FulfillPromise(promise, resolution).
    2. Return undefined.
  13. Let thenJobCallback be HostMakeJobCallback(thenAction).
  14. Let job be NewPromiseResolveThenableJob(promise, resolution,thenJobCallback).
  15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
  16. Return undefined.

There are quite some steps

  1. it checks if promise is already resolved, return if so
  2. it forbids resolve() to be called with promise itself, it creates infinite chaining
  3. resolves to the value if the argument is not Object
  4. if the argument is thenable, chain them

The step 5 clearly tells us that resolve() does nothing if promise is already resolved.

Promise.prototype.finally(onFinally)

Here are the steps involved in Promise.prototype.finally(), based on ECMAScript Spec.

  1. Let promise be the this value.
  2. If promise is not an Object, throw a TypeError exception.
  3. Let C be ? SpeciesConstructor(promise, %Promise%).
  4. Assert: IsConstructor(C) is true.
  5. If IsCallable(onFinally) is false, then
    1. Let thenFinally be onFinally.
    2. Let catchFinally be onFinally.
  6. Else,
    1. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called:
      1. Let result be ? Call(onFinally, undefined).
      2. Let promise be ? PromiseResolve(C, result).
      3. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called:
        1. Return value.
      4. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »).
      5. Return ? Invoke(promise, "then", « valueThunk »).
    2. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »).
    3. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called:
      1. Let result be ? Call(onFinally, undefined).
      2. Let promise be ? PromiseResolve(C, result).
      3. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: 1.Return ThrowCompletion(reason).
      4. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »).
      5. Return ? Invoke(promise, "then", « thrower »).
    4. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »).
  7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).

From step 6, we can see that internally finally() use then() callbacks to create new Promises, also thenFinallyClosure always returns the value being passed, which means the return value of onFinally doesn't affect the fulfillment.

You might also be able to find a solution fromcommunity posts or fromAI solution.