“I know it is a bad thing to break a promise, but I think now that it is a worse thing to let a promise break you.”
― Jennifer Donnelly, A Northern Light
Introduction
I have to mention this: Please re-read the quote above. I can’t stop laughing! Hope you have a good laugh also.
Anyway. A promise is an object which returns a value, but one must wait for some time until it returns. It’s a fitting name, like the program has to go fetching a value and “promises” to come back with a value after a while. And it’s also realistic, it might succeed or it might not.
When a promise is fulfilled, one can call then()
, to trigger a callback function with the value obtained from the promise.
When a promise is rejected, calling catch()
will trigger a callback function which contains the rejection information.
Lastly, calling finally()
will trigger a callback function, whether a promise is fulfilled or rejected.
The functions then()
, catch()
, and finally()
will return a promise object, which can be used to call any of the mentioned functions. And it can continue on and on, particularly if one uses many then()
s, until reaching the solution one wants. This is promise chaining.
Nested Promises
Suppose we have the following functions:
const add = (x, y) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (x == 0 || y == 0){
reject('Nothing to be added!');
}
resolve(x + y);
}, 1000);
});
};
const subtract = (x, y) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (x - y < 0){
reject('Result cannot be less than zero!');
}
resolve(x - y);
}, 1000);
});
};
What both functions do is calculate something after 1000ms, then return the result if it satisfies the conditions, or reject the result if it does not. Both functions will return promises as result.
Next, suppose we want to call add()
then subtract()
then add()
again. We do something like this:
add(3, 5)
.then((resAdd) => {
console.log("resAdd: " + resAdd); // resAdd: 8
subtract(resAdd, 2)
.then((resSub) => {
console.log("resSub: " + resSub); // resSub: 6
add(resSub, 10)
.then((resAdd10) => {
console.log("resAdd10: " + resAdd10); // resAdd10: 6
console.log("Finally... It's done");
})
.catch((err3) => console.log("Err3: " + err3));
})
.catch((err2) => console.log("Err2: " + err2));
})
.catch((err) => console.log("Err1: " + err));
This mess of a function does this: After successfully execute add(3, 5)
and return the result, it will proceed to execute subtract(resAdd, 2)
. The result of the subtract(resAdd, 2)
will be used as a parameter for add(resSub, 10)
. After that, it will return the final result.
But it works. There are 3 levels of nested code, and it’s a pain to read all that. What’s more, there are three .catch()
, which basically are the same functions. Surely, we can do better than this! Let’s solve this by using promise chaining.
Promise Chaining
Instead of using nested then()
s, we only need to use the first layer of then()
for every promises. See the snippet below:
add(3,5)
.then((res) => {
console.log("resAdd: " + res); // resAdd: 8
return subtract(res, 2);
})
.then((res) => {
console.log("resSub: " + res); // resSub: 6
return add(res, 10);
})
.then((res) => {
console.log("resAdd10: " + res); // resAdd10: 16
return res;
})
.catch(err => console.log("Error: " + err));
It’s much more readable now. There is only a layer, only one catch()
needed, and we can see the flow of the program much more easily. Oh, we can also make the code even better:
const subtract2 = (res) => {
console.log("resAdd: " + res); // resAdd: 8
return subtract(res, 2);
};
const add10 = (res) => {
console.log("resSub: " + res); // resSub: 6
return add(res, 10);
}
const logFinalResult = (res) => {
console.log("resAdd10: " + res); // resAdd10: 16
};
const logError = err => console.log("Error: " + err);
add(3,5)
.then(subtract2)
.then(add10)
.then(logFinalResult)
.catch(logError);
By defining the callback functions, we can see the code more clearly. It’s more readable for humans.
So, how does it work? The explanation is when a function returns a promise object, it is thenable. Both add()
and subtract()
functions returns a promise object. The then()
function we wrote also returns the result of add()
or subtract()
, so they are also thenable.
Conclusion
Knowing the trick of chaining promises makes your code much more readable. And it’s easier to debug when something goes wrong. It makes your and the programmers who continue your legacy’s life easier. And they will not curse you when you are gone.
I hope this helps you to code better. Live your code to code your life!
P.S. I also love the quote below, have a good laugh!
“Sometimes people don’t understand the promises they’re making when they make them.”
― John Green, The Fault in Our Stars