The Final Guide on Promises JS. Putting together the building blocks one by one.
By: Nicola Hu
Tags: Javascript Tips, JavaScript, Web Development, Software Development, Programming

Part 3. Chaining Of Promises

To conclude the part of the promises, let’s dive into Chaining of Promises. Reading this will give you a better understanding on why you should use promises instead of callbacks.

Intro to Chaining

Motivation: In a realistic scenario, we might want to fetch some data. This might take some time. After the data has been fetched we would like to make other operations, such as extracting some particular informations in the data, manipulating it, etc...

This is preferably done inside the opened pipeline of Promise.then, because, as we’ve seen above, when the pipeline of execution closes, we lose all the internal variables inside the pipeline. Unless using some external variables. Even in this case, we wouldn’t know exactly in the main code when the data has been fetched, so how could operations involving data continue outside of the pipeline?

It seems then natural that the solution is keeping the pipeline of execution of the Promises opened, and do operations one after the other in this thread of execution. So we have the concept of Chaining of Promises. As an intuitive idea of example code:



Promise.then(handlers1).then(handlers2)

Easier to remember as Promise.then.then (and eventually more then).

One simple example: ( not a realistic scenario, only for learning purposes).

Let’s say we want to wait 3 seconds and then to sum two numbers, x and y. After another 3 seconds we want to multiply the result by 10 and print it.

The code will be (do not worry if you do not understand it now. By the end of this part everything will be clear):

let x = 1;

let y = 2; let firstTask = new Promise((res, rej) => {

setTimeout(() => {res(x+y)}, 3000) //wait 3s and then resolve

}) function secondTask(result) {

return new Promise((res, rej) => {

setTimeout(() => {res(10*result)}, 3000)// w. 3s & resolve.

})

} firstTask.then(secondTask).then(console.log)

This will print 30 (= 3*10) after 6 seconds. We chained two operations. We will analyze the code after explaining the concepts.

The main idea of Promises chaining is that Promise(taskFunction) will execute the taskFunction until it is solved (or rejected).

The first following “then”, with the callbackHandlers (i.e. the callbackSuccess and, if specified, the callbackFailure) will handle the result.

Subsequently, the second following “then” will receive something from the previous then and will handle it with his callbackSuccess function (eventually also a callbackFailure). And so on. Understanding how and what do the followings “then” receive as result is the main objective of this part.

Promise.then is a Promise

First important fact. When we have a Promise object and we apply to it its “then” method, not only the taskFunction and the callbackHandlers will be executed. The Promise.then returns an other Promise object. You can test it in your console if you’re not convinced:

let examplePromise = new Promise((res, rej) => {

res(2)

}) console.log(examplePromise.then((result) => { return result+2 }))

// still a Promise!

But, this should be reasonable: the then method, after all, is a method appliable to the Promise objects. So it does make sense that if we want to keep applying other then, having something like Promise.then.then, the Promise.then should return a Promise (or see it as a Promise itself).

From now on, we can refer to the Promise object “Promise.then” as the “then-Promise”. This will help us differentiate from the Main original Promise, which we will refer to as “main-Promise”. So in the example above, examplePromise is our main-Promise, while examplePromise.then(...) is our then-Promise.

In order to understand how the chain works, it’s enough (you will get this point later) understanding what we get as then-Promise (Is it solved? Is it rejected? With which values?), from the main-Promise and the handlers functions (callbackSuccess, callbackFailure).

When we figured out the status (solved, rejected, pending) and the value of the then-Promise, we figured out everything about the then-Promise. So let’s see how we can deduce them.

There are 3 different general cases that we need to understand in order to figure out what the then-Promise is.

1st case

The callbackHandler returns anything that is not a Promise (AND there is no error raised in the callbackHandler, be it the callbackSuccess or the callbackFailure). Subsequently, the “then-Promise” will have status of SOLVED and as VALUE the value returned by the callbackHandler.

Small remark about terminology:

If you are confused all the times that I use the word callbackHandler. With callbackHandler I refer to the callbackSuccess if the main-Promise is successful, in which case the callbackSuccess will be executed. I refer to the callbackFailure if the main-Promise is rejected, in which case the callbackFailure will be executed. Only one of them will be executed.

Let’s suppose that the main-Promise resolves the taskFunction. Since it is successful, the callbackSuccess will be executed. The callbackSuccess, if returns anything that is not a Promise (and doesn’t raise any errors) then the then-Promise will be solved (status of resolved) with value, exactly the value returned (if nothing is returned by the callbackSuccess function. Then it is undefined the value).

This behaviour holds true even if the main-Promise is rejected (status rejected) and the callbackFailure that is going to be called, doesn’t raise/throw any errors.

In this case the value will be what is returned by the callbackFailure, if it doesn’t throw any error/exception and the status is always solved.

Example 1:

let examplePromise = new Promise((res, rej) => {

res(2)

}) examplePromise.then(console.log) // the then-Promise

The “then-Promise” is a resolved Promise, with value undefined, since the callbackSuccess (in this case is the console.log function) doesn’t return anything.

Example 2: with callbackFailure

let examplePromise = new Promise((res, rej) => {

rej(2)

}) examplePromise.then(console.log, console.log) // the then-Promise

Notice that the callbackSuccess and the callbackFailure are both the console.log function.

First of all, in this case, the main-Promise will be rejected! (rej(2) in the taskFunction code). So the callbackFailure will be executed. But the console.log doesn’t raise any errors. So… We’re in the conditions of this case.

What will the then-Promise be?

The then-Promise is a resolved Promise with value undefined.

Resolved because regardless of what is executed (be it the success or the failure callback), it’s the returned value (if any) of the callbackHandler function that will determine the status of the then-Promise, if no errors are raised in the callbacks.

Undefined because console.log function doesn’t return anything.

Little note: for those who wonder why we set reject a value that is not an error (2), this is usually not the case. In practical applications you always reject errors.

2nd case

If the callbackHandler (be it the success or failure callback) throws an error during the execution, “then-Promise” will be rejected, with value the error raised.

Example:

let examplePromise = new Promise((res, rej) => {

res(2)

}) function successCallback(result) {

throw new Error('throwing an error!')

} examplePromise.then(successCallback) // the then-Promise

The main-Promise is resolved with value 2. Therefore the successCallback will be executed (we didn’t even bother defining the failureCallback). We can see that the successCallback function raises an error (for those who do not know, the syntax “throw new Error(..)” raises an error. This is all you need to know).

Notice: we are throwing errors in the successCallback! We can throw errors whereever we want. It would be the same if we threw errors in the failureCallback.

In this case, the then-Promise will be a rejected Promise and value the error raised. (status: rejected; value: the error thrown).

The same output would have happened even if the taskFunction ended with rejection, calling the failureCallback, and the failureCallback threw an error.

Little note: there are some more details to errors that will come later. They’re not really important. Focus on the logic for now and do not get lost into details. Below I will discuss also the minor things such as: absorbing errors, the catch, etc…

3rd Case: the callbackHandler returns a Promise (and no errors are raised). In which case the then-Promise will be exactly the Promise returned.

The example is exactly the one we displayed at the beginning of this Part.

let x = 1;

let y = 2; let mainPromise = new Promise((res, rej) => {

setTimeout(() => {res(x+y)}, 3000) //wait 3s and then resolve

}) function secondTask(result) {

return new Promise((res, rej) => {

setTimeout(() => {res(10*result)}, 3000)// w. 3s & resolve.

})

} mainPromise.then(secondTask).then(console.log)

Let’s start by answering few questions: As exercise you should try to answer first. Then read the solution.

Q1) The then-Promise, in this case is the Promise object “mainPromise.then(secondTask)”, is it solved or rejected?

Q2) With which value?

Q3) Is the then-Promise immediately solved/rejected, or first is pending?

Answers:

The then-Promise is a Promise. We’re in the third case, specifically: the successCallback (it could have also been the failureCallback), in this case secondTask function, returns a Promise. Therefore the then-Promise will be exactly the Promise returned. It will be a resolved Promise (after some seconds) with value 30.

The then-Promise is instanciated after three seconds. This is because the main-Promise still hasn’t been solved. It will be solved only after 3 seconds with value 3.

Once that the then-Promise is initialized, for 3 seconds, this Promise will have status of pending. After these 3 seconds it will be solved with value 30.

Finally, the second and final “then” (.then(console.log)), will send the value 30 as input to the callbackSuccess function, which is console.log in this case.

Returning to the bigger Picture of Chaining:

The problem reduced: instead of main-Promise.then.then, we have reduced to: then-Promise.then, as “then-Promise = main-Promise.then”. And we perfectly know how to deal with only one then.

So if we have the main-Promise and 4 then, one by one we can reduce it. First, understanding what the “then-Promise” is. So once figured it out, we will have a “then-Promise” and 3 then. Then we can continue and repeat the same logic for the nexts then.

This concludes the chaining part.

Once you have more “then”, you know how to reduce one by one, by figuring out the “then-Promise” time by time. Thus the chaining Process becomes a cyclic procedure and you just need to know a single procedure, which reduces to the Promise basics seen at the beginning.

You should exercise. Try immagining some exercise that do involve a process of tasks to be executed. Try to figure out how many then you need, and how to split the problems into single pieces of tasks.

These topics are not easy and practicing is highly recommended to master them.

If you wish I could put at the end a list of exercises.

Wrapping up

So the pipeline of some Promises chaining is recycling according to a single Promise process. The pipeline will end after the last callback in the last “then”.

Take away of Promise-chaining: know how to figure out the status and value of the then-Promise.