Promises Chaining
Learning outcomes:
- What is chaining
- How chaining works internally
- A practical example
What is chaining?
Yes, JavaScript promises are superbly amazing, but this isn't true until and unless we discover the concept of chaining. But what exactly is chaining?
then()
on the promise returned by then()
.Sounds crazy? Let's discuss it!
Recall from the previous chapter that the method then()
puts up a callback, internally within a queue in its promise, to be executed the moment it settles (resolves or fails).
The whole idea sounds quite sensible: 'doSomeAsyncTask()
and then once it completes, do this with the result'. In very simple words, then()
puts up a callback to fire only once its owner promise resolves.
Talking about the return value, appreciate the fact that then()
returns a new promise whose settlement depends on the invocation and, consequently, completion of the provided callback argument.
When we invoke then()
on a promise p
, with a given argument callback, a new promise is created and returned that only settles when the callback is fired (and this happens only once the main promise p
settles).
It's like we now say: 'doSomeAsyncTask()
, then do this with the result; and once this is done; then further do this'.
This return of a promise by then()
is what enables subsequent then()
calls to be made on its invocation expression; and thus the whole idea of chaining.
How chaining works?
Promise chaining is not that straightforward as much as it looks. Multiple checks are made internally within the then()
method after which it's decided how the returned promise will settle and with what value, and that how will even subsequent promises settle down.
In this section we shall understand all the technical details to chaining promises using the then()
method, before moving on to consider some real examples on the topic.
As we know from the previous Promise Basics chapter, if then()
is called while its promise is still unsettled, the callbacks are queued internally - to be executed when the promise settles in the future.
Now what happens after this is that:
But why do we need to return another promise?
Well, think logically - if an async task wrapped up in a promise object completes and likewise its then()
callback fires, it could be the case that we need to perform a second async operation at this point.
doFirstAsyncTask()
and then once it completes doSecondAsyncTask()
'.Thereby it sounds sensible to return a new promise by calling then()
, to placehold the eventual completion of the respective callback's actions.
According to the spec, this returned promise is given the name derived promise. In the discussion that follows, we'll use this name to refer to any promise returned by then()
.
A reference to this derived promise is saved internally inside the main promise, just like the onFulfilled
(or onRejected
) callback is saved, given that the main promise is still unsettled.
Each invocation of then()
creates a separate promise that depends on the callbacks provided to that specific then()
method.
What this means is that multiple then()
calls on a promise p
will each return a completely new promise, which will settle when the corresponding callback's actions are complete.
For example calling p.then(f1, r1)
will return a promise which will depend on the completion of the action f1
(or r1
) for its resolution (or rejection). Similarly calling p.then(f2, r2)
will return another promise, but this time dependent upon the callback f2
(or r2
).
This signifies the fact that each call to then()
with given onFulfilled
and onRejected
arguments, queues up the callbacks and along with them the reference to the derived promise, internally within the main promise.
Let's also talk about the case when then()
is called after a promise's settlement.
First the respective callback is executed; then its returned or thrown value is made the value of the derived promise and used to, therefore, settle it; before finally returning this derived promise.
Returned promise's settlement
Essentially, when a callback passed to then()
completely executes, its corresponding derived promise is settled down with its return (or thrown) value.
The only case where things are a bit different is when the callback returns a promise itself - in that case the returned promise governs the derived promise.
There are basically three possible outcomes of firing the callback passed to then()
:
- Returning a non-promise value
- Throwing an exception
- Returning a promise
Let's discuss all these three possible outcomes in detail.
A non-promise value
If the callback returns a non-promise value - for example a number, a string, an array etc. - the returned promise is fulfilled with that value.
Following is an illustration:
var p = new Promise(function(resolve, reject) {
resolve("Data1");
});
var p2 = p.then(function(data) {
// callback returns a non-promise value
// in this case a string
return "Data2";
});
console.log(p2);
A promise p
is created and immediately fulfilled in line 2, after which the then()
method is called on it. Since p
is settled, the passed-in callback is executed right away, returning the value "Data2"
. This results in the derived promise, saved in p2
, to be fulfilled with the value "Data2"
.
Consider another example below:
var p = new Promise(function(resolve, reject) {
reject("Sorry");
});
var p2 = p.then(null, function(data) {
// callback returns a non-promise value
// in this case a string
return "OK";
});
console.log(p2);
As before, a promise p
is created but this time immediately rejected in line 2, after which the then()
method is called on it. Since p
is settled, the passed-in callback is executed right away, returning the value "OK"
. This results in the derived promise to be fulfilled, once again, with the value "OK"
.
It's simple:
An exception thrown
If the callback throws an exception - for example throw new Error("Sorry")
- the returned promise is rejected with that value (or better to say, with that reason).
Following is an illustration:
var p = new Promise(function(resolve, reject) {
resolve("OK");
});
var p2 = p.then(function(data) {
// callback throws an error
throw "Sorry";
});
console.log(p2);
A promise p
is created and immediately fulfilled in line 2, after which the then()
method is called on it. Since p
is settled, the passed-in callback is executed right away, throwing the value "Sorry"
. This results in the derived promise, saved in p2
, to be consequently rejected with the value "Sorry"
.
Similar to this, consider the example below:
var p = new Promise(function(resolve, reject) {
reject("Sorry");
});
var p2 = p.then(null, function(data) {
// callback throws an error
throw "Sorry again";
});
console.log(p2);
Everything here is the same as before, except for that the first promise p
is now instead rejected with the value "Sorry"
.
This results in executing the second argument to then()
which throws an error saying "Sorry again"
. Consequently the derived promise is rejected with value "Sorry again"
.
A promise value
If the callback returns a promise itself, we have a special case - the promise returned by then()
abides by the result of this promise.
In other words - if the returned promise fulfills, the derived promise fulfills too; if it's rejected, the derived promise is rejected too; and if it's pending, the derived promise is also put in the pending state.
Consider the following code:
var p = new Promise(function(resolve, reject) {
resolve("OK");
});
var p2 = p.then(function(data) {
// callback returns a promise
return new Promise(function(resolve, reject) {
resolve(data + " Bye");
});
});
console.log(p2);
A practical example
Yes, chaining is definitely a great way to carry out successive asynchronous procedures, one after another, in an extremely neat syntax.
We saw a couple of examples above, that illustrated the idea of chaining from all perspectives, but still left a question answered: is there a real use of chaining promises together?
Let's fulfill this question!
Can you recall the 'nested AJAX requests' task we performed in the Promises Introduction chapter? That's one application - request for a file; then once it's received, parse it; and ultimately make another request after it.
Previously we solved this task using callbacks which imposed syntactic clutter in the code, but now we'll solve it using the concept of chaining promises. Consider the code below:
names.txt
please refer to Promises Introduction.var request1 = new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "names.txt", true);
xhr.onload = function(e) {
if (this.status === 200) { resolve(this); }
}
xhr.send();
});
var request2 = request1.then(function(data) {
// extract the filename from names.txt
var filename = data.responseText.split("\n")[1];
// second async operation
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open("GET", filename, true);
xhr.onload = function(e) {
if (this.status === 200) { resolve(this); }
}
xhr.send();
});
});
request2.then(function(data) {
alert(data.responseText);
});
In the promise request1
we dispatch out and handle our first AJAX request, to names.txt
. When this request completes we call resolve()
with the xhr
object as an argument (in line 5).
This leads to calling the then()
callback given in line 10, which processes xhr
's response and, in turn, puts up a second request using it.
data
in line 10 is the xhr
object in line 2.The callback returns another promise, saved in request2
. When this second request also completes request2
fulfills, ultimately executing the callback in line 25 which alerts the response of this second request.
To boil it down - we chained two promises request1
and request2
together with the second one dependent on the settlement of the first one. As you might appreciate now, chaining is indeed an extremely powerful and neat way to accomplish successive asynchronous operations.
Rewrite the code above, using a function makeRequest()
that takes in a filepath and returns a promise wrapping up an AJAX request to it.
Following is the boilerplate set up for you:
function makeRequest(filepath) {
// write your code here
}
The filepath
argument here is a string that holds the path of the file to request for (which you can directly pass to the open()
method).
function makeRequest(filepath) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open("GET", filepath, true);
xhr.onload = function() {
if (this.status === 200) { resolve(this); }
}
xhr.send();
});
}
var request1 = makeRequest("names.txt");
var request2 = request1.then(function(data) {
// extract the filename from names.txt
var filename = data.responseText.split("\n")[1];
return makeRequest(filename);
});
request2.then(function(data) {
alert(data.responseText);
});
Moving on..
To summarise it all, chaining is crucial to understanding the whole purpose of promises to the core. If you can get the hang of chaining, then you can get the hand of literally anything in promises. Belive it!
In particular, you shall know that then()
returns a new promise object which makes the entire idea of chaining possible; and the mechanism that goes on internally in the method whereby references to the derived promises are stored.
So to end it up, make sure you understand everything in this chapter and are hence able to clear the next quiz with a 100% score. Keep learning and keep making yourself better and better!