The Async Series: Promises

snickdx

Nicholas Mendez

Posted on August 11, 2021

The Async Series: Promises

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");
   });
});
Enter fullscreen mode Exit fullscreen mode

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.

image

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);
}
Enter fullscreen mode Exit fullscreen mode

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");
     })  
  })
Enter fullscreen mode Exit fullscreen mode

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);
  }  
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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'
  });
Enter fullscreen mode Exit fullscreen mode

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]
  })
Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
snickdx
Nicholas Mendez

Posted on August 11, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

The Async Series: Promises
codenewbie The Async Series: Promises

August 11, 2021

"Do you know what '++' does?"
codenewbie "Do you know what '++' does?"

June 29, 2021

JavaScript Object.fromEntries()
codenewbie JavaScript Object.fromEntries()

May 27, 2020

How to Trim String in JavaScript
codenewbie How to Trim String in JavaScript

April 15, 2020