BFE.dev solution for JavaScript Quiz
3. Promise then callbacks

TL;DR

For Promise.prototype.then():

  • if function is passed as callback, the return value of the function is used as the fulfillment value
  • for non-function values, previous fulfillment value is used.
Promise.resolve(1)            // fulfilled : 1
  .then(() => 2)              // function is passed, return value is 2 => fulfilled : 2
  .then(3)                    // non-function is passed, previous fulfillment value 2 is used => fulfilled : 2
  .then((value) => value * 3) // function is passed, value is 2, return value is 6 => fulfilled: 6
  .then(Promise.resolve(4))   // Promise object is not function, previous fulfillment value 6 is used => fulfilled : 6
  .then(console.log)          // 6 gets logged

Below is longer version of explanation.

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.

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