Search code examples
javascriptevent-loop

why does the Promises result looks like incorrect?


Promise.resolve(1)
    .then(x => console.log(1))
    .catch(x => console.log(2))
    .then(x => console.log(3))

Promise.reject(2)
    .then(x => console.log(4))
    .catch(x => console.log(6))
    .then(x => console.log(7))

result is "1 6 3 7", but if i add another then() before catch with console.log(6) result will be 1 3 6 7, i dont't understand why

Promise.resolve(1)
    .then(x => console.log(1))
    .catch(x => console.log(2))
    .then(x => console.log(3))

Promise.reject(2)
    .then(x => console.log(4))
    .then(x => console.log(5))
    .catch(x => console.log(6))
    .then(x => console.log(7))

result is 1 3 6 7

i'm trying to understand how it works


Solution

  • Promise listeners are run by a Promise task runner job put in the Promise Job Queue when a promise becomes fulfilled or rejected. HTML 5 uses the microtask queue for the Promise Job Queue, and the queue operates as a FIFO (first in first out).

    Promise Jobs are run consecutively, after other JavaScript has returned to the event loop, until the Promise Job queue is empty.

    The purpose of a promise job is to pass the value or rejection reason of the promise that created the job onto the next promise in a chain of promises.

    If the promise spawning the job is fulfilled, the next promise will be fulfilled with the same value, and if rejected, rejected with the same reason. If a promise listener returns a promise, the next promise is resolved with the promise returned.

    Passing on promise state and value to next promises is performed using internally stored¹ values of their resolve and reject functions.

    It's important to note that catch clauses are shorthand² for a two argument call to then. Whether a clause in the source code says then or catch, promise jobs are still created to resolve, fulfill or reject the promise object that the clause returned.

    Putting all this together should allow predicting the sequence of promise listener executions in cases when multiple promise chains are being processed at the same time.

    The first example inter-meshes promise jobs in the following sequence:

    1. log(1), not log(4)
    2. not log(2), log 6
    3. log(3), log(7)

    The second example also inter-meshes program jobs but with a different alignment:

    1. log(1), not log(4)
    2. not log(2), not log(5)
    3. log(3), log(6)
    4. log( 7)

    Moral of the story? Don't depend on the execution order of multiple jobs in the promise queue, by breaking promise chains into smaller chains if you need partial completion results.


    ¹ About resolve and reject functions for chained promises.

    • Promise objects internally store the resolve and reject functions for promises returned by calling their then, catch and finally methods.
    • Promise jobs use these stored functions to resolve, fulfill or reject next promises in a chain.
    • the stored functions internally reference the promises they were issued for. Typically this is what holds chained promises in memory and prevents them being garbage collected before settlement.

    ² About then calls with one argument

    • then(listenerFunction) is equivalent to then(listenerFunction, null). Applied to a rejected promise, null as the rejection handler causes the next promise in a chain to be rejected with the same reason.
    • catch(listenerFunction) is equivalent to then(null, listenerFunction). Applied to a fulfilled promise, null as the fulfillment handler causes the next promise in a chain to be fulfilled with the same value.

    Promise chain clauses that don't apply to the settled state of a previous promise in a chain are not skipped - they actively pass on the state and value of the previous promise to the next promise within a promise job entered into the microtask queue for that purpose.