Seph Reed Seph Reed - 7 months ago 11
Javascript Question

Why are javascript promises asynchronous when calling only synchronous functions?

In testing I've found that JavaScript Promises are always asynchronous regardless of whether or not they contain any asynchronous functions in their chain.

Here is some code that shows the order of operations in console. If you run it you will see that even though every function is synchronous the output shows both of the

aPromise()
calls being run in parallel, and
"surprisingly this happens after run 2 finishes"
not happening before run 2 finishes.



function aPromise() {
return new Promise(function(resolve, reject) {
console.log("making promise A")
resolve(bPromise());
console.log("promise A resolved")
});
}


function bPromise() {
return new Promise(function(resolve, reject) {
console.log("making and resolving promise B")
resolve();
});
}

aPromise().then(function() {
console.log("finish run 1");
}).then(function() {
console.log("surprisingly this happens after run 2 finishes");
});
aPromise().then(function() {
console.log("finish run 2");
})





Output to console:

making promise A
making and resolving promise B
promise A resolved
making promise A
making and resolving promise B
promise A resolved
finish run 1
finish run 2
surprisingly this happens after run 2 finishes


So, Why are JavaScript promises asynchronous when calling only synchronous functions? What is happening behind the scenes that leads to this behavior?




P.S. In order to better understand this I implemented my own Promise system and I found that making synchronous functions happen in the expected order was easy but making them happen in parallel was something I could only accomplish by putting a setTimeout() of a few milliseconds at every resolve (My guess is that this is not what's happening with vanilla promises and that they are actually being multi threaded).

This has been a small problem for one of my programs where I'm traversing a tree applying an array of functions to each node and putting the functions in queue if that node has an asynchronous function already running. Most of the functions are synchronous so the queue is rarely used but upon switching over from callbacks (hell) to Promises I've been having an issue where the queues get used almost always as a result of Promises never running synchronously. It's not a huge problem but it is a bit of a debugging nightmare.

Answer

The callback passed to a Promise constructor is always called synchronously, but the callbacks passed into then are always called asynchronously (you could use setTimeout with a delay of 0 in a userland implementation to achieve that).

Simplifying your example (and giving the anonymous function's names so I can refer to them) to:

Promise.resolve().then(function callbackA () {
  console.log("finish run 1");
}).then(function callbackB () {
  console.log("surprisingly this happens after run 2 finishes");
});

Promise.resolve().then(function callbackC () {
  console.log("finish run 2");
})

Still gives the output in the same order:

finish run 1
finish run 2
surprisingly this happens after run 2 finishes

Events happen in this order:

  1. The first promise is resolved (synchronously)
  2. callbackA is added to the event loop's queue
  3. The second promise is resolved
  4. callbackC is added to the event loop's queue
  5. There is nothing left to do so the event loop is accessed, callbackA is first in the queue so it is executed, it doesn't return a promise so the intermediate promise for callbackB is immediately resolved synchronously, which appends callbackB to the event loop's queue.
  6. There is nothing left to do so the event loop is accessed, callbackC is first in the queue so it is executed.
  7. There is nothing left to do so the event loop is accessed, callbackB is first in the queue so it is executed.

The easiest way I can think of to work around your problem is to use a library that has an Promise.prototype.isFulfilled function you can use to decide whether to call your second callback synchronously or not. For example:

var Promise = require( 'bluebird' );                                                                                                                          

Promise.prototype._SEPH_syncThen = function ( callback ) { 
    return (
      this.isPending()
        ? this.then( callback )
        : Promise.resolve( callback( this.value() ) ) 
    );  
}

Promise.resolve()._SEPH_syncThen(function callbackA () {
  console.log("finish run 1");
})._SEPH_syncThen(function callbackB () {
  console.log("surprisingly this happens after run 2 finishes");
});

Promise.resolve()._SEPH_syncThen(function callbackC () {
  console.log("finish run 2");
})

This outputs:

finish run 1
surprisingly this happens after run 2 finishes
finish run 2
Comments