Search code examples
javascriptes6-promise

Why does JS promise print all the resolve first then rejects second


Why does promise print all the success first then the rejects after, even though i wrote the code for it to appear randomly

var errors = 0;
var successes = 0;
var p1;
for (var i = 0; i < 10; i++) {
  p1 = new Promise(function(resolve, reject) {
    var num = Math.random();
    if (num < .5) {
      resolve(num);
    } else {
      reject(num)
    }
  });

  p1.then(function success(res) {
    successes++;
    console.log("*Success:* " + res)
  }).catch(function error(error) {
    errors++
    console.log("*Error:* " + error)
  });
}

OUTPUT

VM331:16 *Success:* 0.28122982053146894
VM331:16 *Success:* 0.30950619874924445
VM331:16 *Success:* 0.4631742111936423
VM331:16 *Success:* 0.059198322061176256
VM331:16 *Success:* 0.17961879374514966
VM331:16 *Success:* 0.24027158041021068
VM331:19 *Error:* 0.9116586303879894
VM331:19 *Error:* 0.7676575145407345
VM331:19 *Error:* 0.5289135948801782
VM331:19 *Error:* 0.5581542856881132

Solution

  • It has to do with how asynchronous code works

    .then().catch() - has to wait for the queue twice (hmm, I need to explain this)

    .then() only once

    Promises are by nature asynchronous ... in your code, when a promise resolves, the .then code is put on the microtask? queue ... and processed in turn

    when it rejects, as .then has no onRejected callback, so, the next handler in the promise chain .catch in your case is added to the microtask? queue - but by then, all the .then codes have been executed

    try using .then(onSuccess, onError) and you'll get what you expect

    var errors = 0;
    var successes = 0;
    var p1;
    for (var i = 0; i < 10; i++) {
        p1 = new Promise(function(resolve, reject) {
            var num = Math.random();
            if (num < .5) {
                resolve(num);
            } else {
                reject(num);
            }
        });
        p1.then(function success(res) {
            successes++;
            console.log("*Success:* " + res);
        }, function error(error) {
            errors++;
            console.log("*Error:* " + error);
        });
    }

    Another way (at least in native Promises) to get what you are after is

    var errors = 0;
    var successes = 0;
    var p1;
    for (let i = 0; i < 10; i++) {
        p1 = new Promise(function(resolve, reject) {
          setTimeout(() => {
                var num = Math.random();
                if (num < .5) {
                    resolve(`${i} ${num}`);
                } else {
                    reject(`${i} ${num}`)
                }
            });
        });
        p1.then(function success(res) {
            successes++;
            console.log("*Success:* " + res)
        }).catch(function error(error) {
            errors++
            console.log("*  Error:* " + error)
        });
    }

    This is because the setTimeout delays the resolve/reject

    An in-depth explanation

    First things first ... you need to understand that .then is actually

    .then(onFullfilled, onRejected)
    

    and returns a Promise

    next, .catch is simply "syntax sugar" for

    .then(null, onRejected)
    

    in fact, in most Promise libraries (before they went native) it is defined as

    Promise.prototype.catch = function (onRejected) {
        return this.then(null, onRejected);
    };
    

    Right ... so let's look at an un-wound, simple version of your code - and only use THREE promises for brevity

    function executorFunction(resolve, reject) {
        const num = Math.random();
        if (num < .5) {
          resolve(num);
        } else {
          reject(num)
        }
    }
    let successes = 0, errors = 0;
    function success(res) {
        successes++;
        console.log("*Success:* " + res)
    }
    function error(error) {
        errors++
        console.log("*Error:* " + error)
    }
    
    const p1 = new Promise(executorFunction);
    p1.then(success).catch(error);
    
    const p2 = new Promise(executorFunction);
    p2.then(success).catch(error);
    
    const p3 = new Promise(executorFunction);
    p3.then(success).catch(error);

    You can run that and see that it produces the same order of success and error

    Now, let's change it up a bit, so we always get success/fail/success

    function executorFunction(num, fail) {
        return (resolve, reject) => {
            if (fail) {
              reject(num);
            } else {
              resolve(num)
            }
        };
    }
    function success(res) {
        console.log("*Success:* " + res)
    }
    function error(error) {
        console.log("*Error:* " + error)
    }
    
    const p1 = new Promise(executorFunction(1, false));
    p1.then(success).catch(error);
    
    const p2 = new Promise(executorFunction(2, true));
    p2.then(success).catch(error);
    
    const p3 = new Promise(executorFunction(3, false));
    p3.then(success).catch(error);

    This ALWAYS outputs

    *Success:* 1
    *Success:* 3
    *Error:* 2
    

    So we see the order you are seeing in your question - so far so good

    Now, let's rewrite the .then/.catch in there expanded form

    function executorFunction(num, fail) {
        return (resolve, reject) => {
            if (fail) {
              reject(num);
            } else {
              resolve(num)
            }
        };
    }
    function success(res) {
        console.log("*Success:* " + res)
    }
    function error(error) {
        console.log("*Error:* " + error)
    }
    
    const p1 = new Promise(executorFunction(1, true));
    p1.then(success, null).then(null, error);
    
    const p2 = new Promise(executorFunction(2, false));
    p2.then(success, null).then(null, error);

    let's only use two promises, the FIRST rejecting ... we know that this will output success 2 then error 1 - i.e. in the reverse order we are expecting

    So let's analyse what is happening

    Because you're resolving/rejecting synchronously in the Promise constructor executorFunction

    const p1 = new Promise(executorFunction(1, false));
    

    is immediatelty a resolved Promise - fulfilled as 2 for p2, and rejected with reason 1 for p1, but it's never in a Pending state. So, when a promise is not pending (it's resolved, but that means either fulfilled or rejected, but the terminology has been mixed up, so I'll keep saying "not pending"), any "handler" is added to the microtask queue - so at the end of all that code, the microtask queue looks like

    **microtask queue**
    (resolved:2).then(success, null).then(null, error); // added second
    (rejected:1).then(success, null).then(null, error); // added first
    

    Now the JS engine, since there's nothing running anymore, processes the microtask queue (the head of the queue is at the bottom by the way)

    • it sees a rejected promise(1), but the .then has no on rejected function, so the promise value carries down the chain
    • .then returns this rejected promise with the original rejection reason
    • this promise, because it has a handler (.catch in the original code) is added to the microtask queue

    .

    **microtask queue**
    (rejected:1)>(rejected:1).then(null, error);         // added third
    (resolved:2).then(success, error).then(null, error); // added second
    

    Now the next microtask is processed

    • It sees a resolved promise(2) so calls success
    • outputs success 2
    • .then returns a promise, because your success function has no return, this is return undefined and the promise is resolved as undefined
    • this promise, because it has a handler (.catch in the original code) is added to the microtask queue

    .

    **microtask queue**
    (resolved:2)>(resolved:undefined).then(null, error); // added fourth
    (rejected:1)>(rejected:1).then(null, error);         // added third
    
    • It sees a rejected promise(1) and there is an onrejected handler calls error
    • outputs error 1
    • .then returns a promise, there's no handler, so nothing is added to the microtask queue

    .

    **microtask queue**
    (resolved:2)>(resolved:undefined).then(null, error); // added fourth
    

    Now the next microtask is processed

    • It sees a resolved promise(2, now undefined) - but there is no onSuccess handler
    • .then returns a promise, there's no handler, so nothing is added to the microtask queue

    .

    **microtask queue**
    **empty**
    

    why using .then(onFullfilled, onRejected) results in the expected order

    OK, so now, if we write the code

    function executorFunction(num, fail) {
        return (resolve, reject) => {
            if (fail) {
              reject(num);
            } else {
              resolve(num)
            }
        };
    }
    function success(res) {
        console.log("*Success:* " + res)
    }
    function error(error) {
        console.log("*Error:* " + error)
    }
    const p1 = new Promise(executorFunction(1, true));
    p1.then(success, error);
    
    const p2 = new Promise(executorFunction(2, false));
    p2.then(success, error);

    The microtask queue starts, like

    **microtask queue**
    (resolved:2).then(success, error); // added second
    (rejected:1).then(success, error); // added first
    

    Now the next microtask is processed

    • It sees a rejected promise(1) so calls error
    • outputs error 1
    • .then returns a promise, there's no handler, so nothing is added to the microtask queue

    .

    **microtask queue**
    (resolved:2).then(success, error); // added second
    
    • It sees a resolved promise(2) so calls success
    • outputs success 2
    • .then returns a promise, there's no handler, so nothing is added to the microtask queue

    .

    **microtask queue**
    **empty**
    

    why adding setTimeout results in the expected order

    Now let's change the executorFunction to add the setTimeout

    function executorFunction(num, fail) {
        return (resolve, reject) => {
            setTimeout(function() {
                if (fail) {
                  reject(num);
                } else {
                  resolve(num)
                }
            });
        };
    }
    function success(res) {
        console.log("*Success:* " + res)
    }
    function error(error) {
        console.log("*Error:* " + error)
    }
    
    const p1 = new Promise(executorFunction(1, true));
    p1.then(success, null).then(null, error);
    
    const p2 = new Promise(executorFunction(2, false));
    p2.then(success, null).then(null, error);

    Again, for brevity, let's use only TWO promises, and the first one fails, because we know that the output in the original code would be success 2 then fail 1 Now we have two queues to consider ... microtask and "timer" - the timer queue has lower priority than the microtask queue ... i.e. when there's nothin running (immediately) then JS will process the microtask queue until it is empty, before even trying the timer queue

    So - here we go then

    At the end of that code we have

    ** empty microtask queue **                             timer queue
                                                            setTimeout(resolve(2))
                                                            setTimeout(reject(1))
    

    Processing the timer queue, we get a microtask (rejected:1).then(success, null).then(null, error)

    ** microtask queue **                                   timer queue
    (rejected:1).then(success, null).then(null, error)      setTimeout(resolve(2))
    

    Oh, there's something in the microtask queue, lets process that and ignore the timer queue

    • it sees a rejected promise(1), but the .then has no on rejected function, so the promise value carries down the chain
    • .then returns this rejected promise with the original rejection reason
    • this promise, because it has a handler (.catch in the original code) is added to the microtask queue

    Oh, there's something in the microtask queue, lets process that and ignore the timer queue

    ** microtask queue **                                   timer queue
    (rejected:1).then(success, null).then(null, error)      setTimeout(resolve(2))
    
    • It sees a rejected promise(1) and there is an onrejected handler calls error
    • outputs error 1
    • .then returns a promise, there's no handler, so nothing is added to the microtask queue

    Now the queues look like

    ** microtask queue **                                   timer queue
                                                            setTimeout(resolve(2))
    

    So, I don't need to go on, right, because error 1 has been output before the second promise chain has even started :p1