Async Simplified, I promise
Callbacks can help with managing the order our async calls. However, things get messy if you have too many. Luckily there's an alternative that definitively shows... some promise.
The Gates of Hell
In the last post of this series, we arrived at the following solution using nested callbacks.
//replace reference to doPrintGreenRed with an anonymous function
printBlue("Blue", function(){
//calls printGreen with our desired parameter
printGreen("Green", function(){
//calls print red with our desired parameter
printRed("Red");
});
});
However, the more calls we need to make to more callbacks we need to define. At some point you will experience a phenomena called callback hell.
Not to mention how messy it will get to perform exception handling in each callback.
try{
printBlue("Blue", function(){
try{
printGreen("Green", function(){
try{
printRed("Red");
}catch(e){
console.error(e);
}
});
}catch(e){
console.error(e);
}
});
}catch(e){
console.error(e);
}
So what now?
In the 6th version of JavaScript released in 2015, promises were released. Instead of accepting callbacks directly, async functions can now return Promise objects.
These promises objects provide then() method that will take the callback and execute it when the main work of the async function is completed.
Luckily, our print functions return promises so our nested callbacks can be rewritten as.
printBlue("Blue")//moved our callback from here
.then(function(){//to here
printGreen("Green")
.then(function(){
printRed("Red");
})
})
We got the desired output. However, is this really an improvement over the callback approach? It still looks very similar. Well the thing about then() is that it returns another promise!
then() returns another promise after the previous one is said to have been resolved.
You can call then() repeatedly to form a what is called a promise chain.
printBlue("Blue")
.then(function(){
//only executes after printBlue() resolves
printGreen("Green");// instead of calling then here
})
.then(function(){ // we call it here
printRed("Red");//only executes after printGreen resolves
})
.catch(e){
console.error(e);
}
Now, the nesting has been flattened, but the main advantage here is the use of the catch() method that is also provided by the promise object.
The catch of the end of the chain will handle any errors that may have been thrown at any part of the chain!
This is a great improvement in terms of readability and error handling.
Making Promises
Just like how we are able to write a higer-order add() we can also write a version of that function that returns a promise. Unlike the printRed/Green/Blue functions, the promise returned by add() will resolve with a value. That value will be received by any function passed to the then() method.
function add(a, b){
//create a promise object
const promise = new Promise(function(resolve, reject){
if(typeof a !== "number" or typeof b !== "number")
reject("Invalid parameter error");//how errors are thrown
else
resolve(a + b);//how values are returned when the work of the function is complete
})
return promise;//return our promise
}
When creating a promise object you need to supply it with 2 callbacks; resolve() and reject().
Instead of using return to return a value we use the resolve() function. What ever is passed to resolve() will be passed to any callback given to then().
Instead of using throw to throw an error we use the reject() function. What ever is passed to reject() will be passed to any callback given to catch().
add(5,10)
.then(function(ans){
console.log(ans);//logs 15
return ans;//passes this value to next then in the chain
})
.then(function(ans){
return add(ans, 5);//returns a new promise to the next then
})
.then(function(ans){
console.log(finalAns);//logs 20
});
add(11, 'cat')
.then(function(ans){
console.log(ans);
//this is not executed because of the type check in the add()
})
.catch(function(error){
console.error(error);//logs 'Invalid parameter error'
});
Any value returned in the callback passed to then() will be passed forward to the callback of the next then() in the chain. That's how the 2nd then() callback was able to receive the result of the 1st then() callback.
Promise.all()
In the examples we have looked at so far, the ordering of our async calls was important so we used then to perform flow control. In cases where our async calls are independent but we need to combine their result of each call in some say, we can use Promise.all().
Promise.all() will execute multiple promises asynchronously but collect their final values in an array.
let promise1 = add(5, 10);
let promise2 = add(11, 12);
let promise3 = add(7, 8);
//Async execution (faster)
Promise.all([promise1, promise2, promise3])
.then(function(result){
console.log(result);// logs [15, 23, 15]
})
Because our additions are independent of each other, we do not use then() to perform the additions synchronously. Instead, they are kept async. This would actually execute faster than synchronizing them.
Important: We only synchronize our calls with then() if the order matters or the calls are dependent on each other.
//Sync execution (slower), not needed in this case
//also relies on global state arr
let arr = [];
add(10, 5)
.then(function(sum1){
arr.push(sum1);
return add(11, 12);
})
.then(function(sum2){
arr.push(sum2);
return add(3, 4)
})
.then(function(sum3){
arr.push(sum3);
console.log(arr);
//logs [15, 23 7] the result of all promises that resolved in the chain
//this result is only available in this scope
});
console.log(arr);
//logs [] empty array because this log runs asynchronously with the first call to add().
//The other promises have not resolved yet.
Conclusion
In this post we showed how Promises improve over nested callbacks by chaining them together. However, the limitation is that the result of all the calls are only available at the end of the chain.
As always you can try any of the code in this article at this REPL.
Is there anyway we can improve on this? If you stick around I promise I will tell in the final post of this series.
Top comments (0)