Understanding javascript promises; stacks and chaining

I've been running into a couple of problems with javascript promises, particularly with stacked chains.

Can anyone explain to me the difference (if there is any!) between these different implementations?

IMPLEMENTATION 1

var serverSidePromiseChain;
serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
}).then(function(response) {
    console.log('2', response);
    return true;
}).then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
}).then(function(response) {
    console.log('4', response);
    return async4();
})

IMPLEMENTATION 2

var serverSidePromiseChain;
serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
});

serverSidePromiseChain.then(function(response) {
    console.log('2', response);
    return true;
})
serverSidePromiseChain.then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
})
serverSidePromiseChain.then(function(response) {
    console.log('4', response);
    return async4();
})

IMPLEMENTATION 3

var serverSidePromiseChain;
serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
});

serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('2', response);
    return true;
})
serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
})
serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('4', response);
    return async4();
})

Does the fact that part of the chain returns a value ('true' in step 2) change the behavior? Do promises require all returned values to be async promises to keep behavior?

Answers:

Answer

You are illustrating the different between chaining and branching. Chaining wil sequence multiple async operations so one starts when the prior one finishes and you can chain an arbitrary number of items to sequence one after the other.

Branching hooks up multiple async operations to all be in flight at the same time when one trigger operation completes.

Implementations 1 and 3 are the same. They are chained. Implementation 3 just uses a temporary variable to chain, whereas implementation 1 just uses the return value from .then() directly. No difference in execution. These .then() handlers will be called in serial fashion.

Implementation 2 is different. It is branched, not chained. Because all subsequent .then() handlers are attached to the exact same serverSidePromiseChain promise, they all wait only for the first promise to be resolved and then all subsequent async operations are all in flight at the same time (not serially as in the other two options).


It may be helpful in understand this to dive one level down into how this works with promises.

When you do (scenarios 1 and 3):

p.then(...).then(...)

What happens is as follows:

  1. The interpreter takes your p variable, finds the .then() method on it and calls it.
  2. The .then() method just stores the callback it was passed and then returns a new promise object. It does not call its callback at this time. This new promise object is tied to both the original promise object and to the callback that it stored. It will not resolve until both are satisfied.
  3. Then the second .then() handler on that newly returned promise is called. Again, the .then() handler on that promise just stores the .then() callbacks and they are not yet executed.
  4. Then sometime in the future, the original promise p gets resolved by its own async operation. When it gets resolved, it then calls any resolve handlers that it stores. One of those handlers will be the callback to the first .then() handler in the above chain. If that callback runs to completion and returns either nothing or a static value (e.g. doesn't return a promise itself), then it will resolve the promise that it created to return after .then() was first called. When that promise is resolved, it will then call the resolve handlers installed by the second .then() handler above and so on.

When you do this (scenario 2):

p.then();
p.then();

The one promise p here has stored resolve handlers from both the .then() calls. When that original promise p is resolved, it will call both of the .then() handlers. If the .then() handlers themselves contain async code and return promises, these two async operations will be in flight at the same time (parallel-like behavior), not in sequence as in scenario 1 and 3.

Answer

Implementation #1 and #3 are equivalent. Implementation #2 differs, as there's no chain there, and all callbacks will be executed on the same promise.

Now let's discuss a little bit about promise chains. The specs tell that:

2.2.7 then must return a promise
2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x)
2.3.3 If x is a promise, adopt its state

Basically calling then on a promise returns another promise, which gets resolved/rejected based on the callback return value. In your case you are returning scalar values, which are then propagated down the chain to the next promise.

In your particular case, here's what happens:

  • #1: you have 7 promises (async call plus 4 then's, plus two from async3()/async4), serverSidePromiseChain will point to the last promise returned by then. Now if the promise returned by async() is never resolved/rejected, then serverSidePromiseChain will also be in the same situation. Same with async3()/async4() if that promise is also not resolved/rejected
  • #2: then is called multiple times on the same promise, additional promises are created however they don't affect the flow of the application. Once the promise returned by async() will be resolved, all the callbacks will be executed, what the callbacks return will be discarded
  • #3: this is equivalent to #1 only now you explicitly pass along the created promises. When the promise returned async() gets resolved, the first callback will be executed, which resolves the next promise with true, the second callback will to the same, the third one will have the chance to convert the chain to a failed one, if async3()'s promise gets rejected, same with the callback that returns async4()'s promise.

Promise chains are best suitable for actual async operations where the operations depend on the results of the previous one, and you don't want to write a lot of glue code, and you also don't want to reach to callback hell.

I wrote a series of articles on my blog about promises, one describing the chaining feature of promises can be found here; the article is targeted for ObjectiveC, however the principles are the same.

Answer

Implementation 1 and 3 appear to be equivalent.

In implementation 2, the last 3 .then() functions all act on the same promise. The .then() method returns a new promise. A fulfilled promise's value cannot be changed. See Promises/A+ 2.1.2.2. Your comment in implementation 2, that response is expected to be true, indicates that you expect otherwise. No, response will not be true (unless that was the value from the original promise).

Let's just try it out. Run the following code snippet to see the differences:

function async(){ return Promise.resolve("async"); }
function async3(){ return Promise.resolve("async3"); }
function async4(){ return Promise.resolve("async4"); }


function implementation1() {
  logContainer = document.body.appendChild(document.createElement("div"));
  console.log("Implementation 1");
  var serverSidePromiseChain;
  serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
  }).then(function(response) {
    console.log('2', response);
    return true;
  }).then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
  }).then(function(response) {
    console.log('4', response);
    return async4();
  });
}

function implementation2() {
  logContainer = document.body.appendChild(document.createElement("div"));
  console.log("Implementation 2");
  var serverSidePromiseChain;
  serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
  });
  serverSidePromiseChain.then(function(response) {
    console.log('2', response);
    return true;
  });
  serverSidePromiseChain.then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
  });
  serverSidePromiseChain.then(function(response) {
    console.log('4', response);
    return async4();
  });
}

function implementation3() {
  logContainer = document.body.appendChild(document.createElement("div"));
  console.log("Implementation 3");
  var serverSidePromiseChain;
  serverSidePromiseChain = async().then(function(response) {
    console.log('1', response);
    return response;
  });
  serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('2', response);
    return true;
  });
  serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('3', response); // response expected to be 'true'
    return async3();
  });
  serverSidePromiseChain = serverSidePromiseChain.then(function(response) {
    console.log('4', response);
    return async4();
  });
}

var logContainer;
var console = {
  log: function() {
    logContainer.appendChild(document.createElement("div")).textContent = [].join.call(arguments, ", ");
  }
};

onload = function(){
  implementation1();
  setTimeout(implementation2, 10);
  setTimeout(implementation3, 20);
}
body > div {
  float: left;
  font-family: sans-serif;
  border: 1px solid #ddd;
  margin: 4px;
  padding: 4px;
  border-radius: 2px;
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.