JavaScript for Testers - Part 2 (Callbacks, Promises and async-await)

asheeshmisra

Asheesh Misra

Posted on October 15, 2023

JavaScript for Testers - Part 2 (Callbacks, Promises and async-await)

Namaskaar 🙏 🙂

This is a two part tutorial on getting up and running with JavaScript, for the testers, so that they can leverage this knowledge while writing test automation scripts.

The way this tutorial has been planned is

  • We begin with covering JavaScript Basics, especially ES6 (that’s part 1)
  • Then we deal with Asynchronous JS (that’s this tutorial)

This is a beginner friendly tutorial for the testers by a tester.

In this part of the tutorial, we focus on Asynchronous JavaScript. To understand the concepts myself, I referred to MDN Docs, multiple videos on Youtube, Udemy and Linkedin learning. I would like to make special mention of Colt Steele and his Udemy course, The Web Developer Bootcamp. Colt is one of the best out there and his this course is like a gold mine for any person who wants to learns Web Development. Some of the examples used in this tutorial draw inspiration from Colt's course.


If you feel lost or confused at any time while reading through this part of tutorial, don't feel bad. This part is indeed confusing. Most of the experts recommend doing it multiple times (reading the stuff and practicing) and it will start making sense. 👼


Introduction

JavaScript is a synchronous, single threaded and blocking language. Synchronous means that only one operation can be in progress at a time. Blocking means that no matter how long a previous process takes, the subsequent process won't start until former is completed. A Thread is simply a process that our JavaScript program can use to run a task. Each thread can do only one task at a time. JavaScript has just one thread called the main thread for executing any code.

In simple words this means that we can have only one operation in progress at a time.

Consider the following example

const founders = () => console.info("Batman founded the Justice League with Superman and Wonder Woman");
const team = () => console.info("Aquaman, Green Lantern, Shazam, Flash, Cyborg joined Justice League");
const war = () => console.info("Justice League fought the war with Darkseid and saved earth from his wrath");

founders();
team();
war();

/*
Output:
Batman founded the Justice League with Superman and Wonder  Woman
Aquaman, Green Lantern, Shazam, Flash, Cyborg joined Justice League
Justice League fought a war with Darkseid and saved earth from his wrath
*/
Enter fullscreen mode Exit fullscreen mode


First we invoke the founders() function which gets added to the callstack. It returns a value and then gets popped off the callstack. Then we invoke team() function which gets pushed to the callstack, it returns a value and get popped out of the callstack. The same happens to the war() function.

Now, imagine that we had some function that fetched data from an API. We don't really know how long is it going to take. Let's assume that it takes 5 seconds for the server to respond. This would mean that the user will have to wait for 5 seconds with an unresponsive website. We would not like that to be the case and therefore, would want to make the function asynchronous in order to prevent the function from blocking the rest of our code.

Asynchronous means that the function(s) is running in the background, performing its tasks, while the other part of our code can still keep running. We should think of asynchronous tasks as tasks that can start now and finish their execution later.

To implement asynchronous code, JavaScript alone is not enough. We need new pieces which are outside the JavaScript to help us write asynchronous code an which is where Web Browsers come into play. Web Browsers define functions and APIs that allow us to register functions that should NOT be executed synchronously, and should instead be invoked asynchronously when some kind of event occurs.

Event, for example, could be

  • Passage of time (setTimeOut or setInterval), OR
  • User's interaction with the mouse (addEventListener), OR
  • Arrival of data over the network (callbacks, promises and async-await)

We can make our code do several things at the same time instead of waiting for main thread to finish.

Common Patterns of Asynchronous Programming

3 most common patterns of asynchronous programming in JavaScript are:

  • Callbacks
  • Promises
  • async - await

Common Patterns of Asynchronous Programming in JavaScript

Let's learn about them one by one.


Callbacks

What are they? In JavaScript, functions are first-class objects. That is, we can work with them in the same way we work with other objects, like assigning them to variables and passing them as arguments into other functions.

A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable. (MDN Docs)

A callback function is a function that is passed as an argument to another function, to be 'called back' at a later time. That is, it is then invoked inside the outer function to complete some kind of routine or action.

Consider the following example. It shows a synchronous callback.

//Old Flash (somewhere in 1940s or 50s) was 'Jay Garrick'
const realNameOfDcHero= (name) =>{
    let realName = "Jay Garrick";
    if (name === "Superman")
        realName = "Clark Kent";
    else if (name == "Batman")
        realName = "Bruce Wayne";
    else if (name == "Wonder Woman")
        realName = "Diana";
    else if (name == "Flash")
        realName = "Barry Allen";
    console.log(`Real name of ${name} is ${realName}`);
}

const getHeroName= (whatsTheName, callback) =>{
    console.log("Getting the Hero's Name. Sit Tight.");
    callback(whatsTheName);
}

console.info("Start");
getHeroName("Wonder Woman", realNameOfDcHero);
console.info("End");

/*
Output:
Start
Getting the Hero's Name. Sit Tight.
Real name of Wonder Woman is Diana
End
*/
Enter fullscreen mode Exit fullscreen mode


NOTE: We need not specify the realNameOfDcHero in the getHeroName function's definition. We could have passed anything, like heroName or callback or cb etc. 

Before we see the asynchronous callback example, we need to understand setTimeout() function. The setTimeout is a JavaScript function that takes two parameters. The first parameter is another function, and the second is the time, in milliseconds, after which that function (passed as first parameter) should be executed.

Ok, now let's dive into the example,

//Example - Asynchronous Callback
const realNameOfDcHero= (name) =>{
    let realName = "Jay Garrick";
    if (name === "Superman")
        realName = "Clark Kent";
    else if (name == "Batman")
        realName = "Bruce Wayne";
    else if (name == "Wonder Woman")
        realName = "Diana";
    else if (name == "Flash")
        realName = "Barry Allen";
    console.log(`Real name of ${name} is ${realName}`);
}

const getHeroName= (whatsTheName, realNameOfDcHero) =>{
    console.log("Getting the Hero's Name. Sit Tight.");
    setTimeout(()=>{
        realNameOfDcHero(whatsTheName);
    },2000);
}

console.info("Start");
getHeroName("Flash", realNameOfDcHero);
console.info("End");
Enter fullscreen mode Exit fullscreen mode


In the example above, we have used the setTimeout function which would execute the callback function after a delay of 2000 milliseconds (or 2 secs). This results in following output:

Example of Asynchronous Callback

/*
Output:
Start
Getting the Hero's Name. Sit Tight.
End
Real name of Flash is Barry Allen
*/
Enter fullscreen mode Exit fullscreen mode


As we can see the statement in the example above executed asynchronously. So now, we know how to simulate asynchronous behaviour in JS. 

Let's see another example. For this example, we will need a web page and a .css file also apart from the .js file. Therefore, we will be creating following 3 files

<!--index.html-->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Learning Async JS - Callbacks</title>
    <link rel="stylesheet" href="css/style.css"/>
</head>
<body>
    <button>Click Me!</button>
    <script src="js/asyncJs.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
/* css/style.css */
button{
    background-color:aqua;
}

.secondaryColor{
    background-color: coral;
    font-family:'Segoe UI';
}
Enter fullscreen mode Exit fullscreen mode
// js/asyncJs.js
const btn = document.querySelector('button');

function toggle(){
    btn.classList.toggle('secondaryColor');
}

toggle();
Enter fullscreen mode Exit fullscreen mode


When we finish creating these files in the order as shown above and save changes and we load the html file in browser, we observe that the colour of button changes immediately. In other words, the toggle() is called instantaneously as soon as the page (re)loads.

What if we don't want this? What if we want it to be called when the button is clicked?

To accomplish this we will need to add an eventlistener and add a callback to it. How?

// js/asyncJs.js
const button = document.querySelector('button');

function toggle(){
    button.classList.toggle('secondaryColor');
}

//toggle();   

button.addEventListener('click', toggle);
Enter fullscreen mode Exit fullscreen mode


We have used toggle() function as a callback function in the addEventListener() on button . 

By the way, the above js code is same as this 😄

//we have used anonymous function
const button = document.querySelector('button');

button.addEventListener('click', function (){
    button.classList.toggle('secondaryColor');
});
Enter fullscreen mode Exit fullscreen mode


and this 😃

// we have used arrow function
const button = document.querySelector('button');
button.addEventListener('click', () =>{
    button.classList.toggle('secondaryColor');
});
Enter fullscreen mode Exit fullscreen mode

Nested Callbacks

Consider we want to see the rainbow colors displayed on our web page in the order : VIBGYOR.

To do so, we write the following code in our JS file

document.body.style.backgroundColor = "violet";
document.body.style.backgroundColor = "indigo";
document.body.style.backgroundColor = "blue";
document.body.style.backgroundColor = "green";
document.body.style.backgroundColor = "yellow";
document.body.style.backgroundColor = "orange";
document.body.style.backgroundColor = "red";
Enter fullscreen mode Exit fullscreen mode


When we reload our web page, we see "red" color only. The script ran so fast that the background colour of web page changed to red before we could even blink.

Let's try to solve this problem by using asynchronous callbacks. We will start small. Delete the above code from the JS file, paste the code below, save changes and refresh the web page.

setTimeout(()=>{
    document.body.style.backgroundColor = "orange";
},1000);

setTimeout(()=>{
    document.body.style.backgroundColor = "red";
},2000);
Enter fullscreen mode Exit fullscreen mode


Ok, it worked 🙌. We saw orange first and then after a second we saw red.

Let's scale it up.

setTimeout(()=>{
    document.body.style.backgroundColor = "violet";
},1000);

setTimeout(()=>{
    document.body.style.backgroundColor = "indigo";
},2000);

setTimeout(()=>{
    document.body.style.backgroundColor = "blue";
},3000);

setTimeout(()=>{
    document.body.style.backgroundColor = "green";
},4000);

setTimeout(()=>{
    document.body.style.backgroundColor = "yellow";
},5000);

setTimeout(()=>{
    document.body.style.backgroundColor = "orange";
},6000);

setTimeout(()=>{
    document.body.style.backgroundColor = "red";
},7000);
Enter fullscreen mode Exit fullscreen mode


When we save the above change to our JS file and reload web page, we see the change in color happen gradually.

However, there is a problem in the above example 😟. We need to keep track of how many milliseconds do we need to wait before displaying a specific color. We can get rid of keeping track of this by nesting the callbacks, like so. Again, starting small

setTimeout(() => {
    document.body.style.backgroundColor = "violet";
    setTimeout(() => {
        document.body.style.backgroundColor = "indigo";
    }, 1000);
}, 1000);
Enter fullscreen mode Exit fullscreen mode


This works!, let's paint the whole rainbow now 🌈.

setTimeout(() => {
    document.body.style.backgroundColor = "violet";
    setTimeout(() => {
        document.body.style.backgroundColor = "indigo";
        setTimeout(() => {
            document.body.style.backgroundColor = "blue";
            setTimeout(() => {
                document.body.style.backgroundColor = "green";
                setTimeout(() => {
                    document.body.style.backgroundColor = "yellow";
                    setTimeout(() => {
                        document.body.style.backgroundColor = "orange";
                        setTimeout(() => {
                            document.body.style.backgroundColor = "red";
                        }, 1000);
                    }, 1000);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);
Enter fullscreen mode Exit fullscreen mode


Wow! this works. However, as we can see, we ended up writing something which is hard to read and difficult to maintain. Such a piece of code is referred to as 'Callback Hell' 😵. 

Let's take one more example and then we will (re) solve the 'callback hell' problem, I 'promise' 😄. Consider making request(s) to a web API. We do not know how much time would it take for the response to come. We might want to do something based upon the response but we would need to wait for the response first. Also, it is not necessary that a response is returned (i.e., request is successful). Our request may timeout due to multiple reasons like network congestion, server busy, site down, incorrect credentials etc.. (request failed). A common pattern (before promises arrived) to make API requests was to pass in two call backs, one would execute if request was successful and other if request failed; and then if the request was successful we may do more request which would result in nesting and soon the code would look like this:

requestDataFromAPI(
    ()=>{
      //this would execute if successful
      requestDataFromAnotherAPI(
        ()=>{
            //this would execute if successful
            requestDataFromYetAnotherAPI(
                ()=>{
                    //this would execute if successful
                },

                () =>{
                    //this would execute if failed
                }
            );
          },
        () =>{
            //this would execute if failed
          }
        );  
    },
    () => {
    //this would execute if failed
    }
  );
Enter fullscreen mode Exit fullscreen mode


This is not only very ugly looking spaghetti code but difficult to understand and maintain also. Let's see a practical implementation:

const requestDataFromAPI = (url, success, failure) => {
  const timeInterval = Math.floor(Math.random() * 3000) + 500;
  setTimeout(() => {
    if (timeInterval > 3000) {
      failure(`Couldn't get the data.`);
    } else {
      success(`Data Retrieved: ${url}`);
    }
  }, timeInterval);
};
Enter fullscreen mode Exit fullscreen mode


The function above fakes request to a fictional API based on DC Comics characters. It also takes in two callbacks and inside the function we check for a variable timeInterval and based upon its value, execute the appropriate callback. Now, let's use it

requestDataFromAPI(
  "dccomics.com/trinity",
  () => {
    //this callback will execute when the request is successful
    console.log("DC Trinity: Superman, Wonder Woman, Batman");
    requestDataFromAPI(
      "dccomics.com/citizensOfThemyscira",
      () => {
        //this callback with execute when the request is successful
        console.log("Queen Hippolyta, Artemis, Cassie Wonder Girl, Donna Troy");
        requestDataFromAPI(
          "dccomics.com/greenlanterncorps",
          () => {
            console.log(
              "Members of Green Lantern Corps; John Stewart, Jessica Cruz, Hal Jordan, AbinSur, Alan Scott, Guy Gardener"
            );
          },
          () => {
            console.log(
              "Oh No!, Sinestro has brought the green lantern corps under his control of fear."
            );
          }
        );
      },
      () => {
        //this callback will execute when the request fails
        console.log("Alas!, Ares won and took over Themyscira");
      }
    );
  },
  () => {
    //this callback will execute when the request fails
    console.log("Trinity is gone, all hail Darkseid!.");
  }
);
Enter fullscreen mode Exit fullscreen mode


In the code above, we first make request to an endpoint of our fictional API dccomics.com to fetch members of Justice League, popularly called as the 'trinity'. If the request is successful, we make request to a different endpoint and if that too is successful we make request to yet another endpoint. When we run this, this is what we see in the browser (the output is based upon random values and therefore is random, i.e. you may not get exactly the same output but it you run multiple times, you will see each of the possible combination).

Nested Callback Example

If the above image is not clear to you, I would encourage you to paste the code for 'requestDataFromAPI()' definition and its usage in async.js file we created above, into your js file, save changes, refresh the web page and check the console tab in the browser.

In the code above we observe the nesting of callbacks and I am sure you would appreciate how difficult it is to understand and debug. 😶

ES 6 made a promise 😅 to get us out of this callback hell. Let's see what that is 😃


Promise

As per MDN docs,

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

A Promise is in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

The lifecycle of a Promise is as follows:

Lifecycle of a Promise


To understand promise better let's use promise to write the above requestDataFromAPI function and then make use of it using promises.

const requestDataFromAPI = (url) => {
  return new Promise((resolve, reject) => {
    const timeInterval = Math.floor(Math.random() * 5000) + 500;
    setTimeout(() => {
      if (timeInterval > 3000) {
        reject(`Couldn't get the data.`);
      } else {
        resolve(`Data Retrieved: ${url}`);
      }
    }, timeInterval);
  });
};
Enter fullscreen mode Exit fullscreen mode


The function above takes in url as a parameter. No callbacks here. Inside its body, the function returns a Promise, which however takes in two callbacks. One would be executed if the promise got resolved and other if it did not, in other words it got rejected.

Let's call this function in our browser's console and save the response to a variable to better understand. After a couple of rounds of execution, we observe, the promise is initially in pending state and after the timeInterval has elapsed, it resolves to either a fulfilled state or a rejected state.

Request from API

Now, let's convert the callback hell in code examples above (well… almost) into promises. 

For the rainbow example, let's first write the function to change the background colour of the webpage, in our JS file.

const paintTheSky = (colour, timeInterval) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      document.body.style.backgroundColor = colour;
      resolve();
    }, timeInterval);
  });
};
Enter fullscreen mode Exit fullscreen mode


The function paintTheSky takes two parameters the color and the timeInterval. Inside the function, we are creating a new promise which also takes in two parameters. First one is a callback which executes when the promise is resolved and the second is a callback which executes when the promise is rejected. They can have any names, not necessarily resolve and reject, but it's just that these names are intuitive and make our code readable. We are calling setTimeout(), which waits for 1 second, changes the color of web page and then resolves the promise.

Since paintTheSky function is returning a promise, we can call .then() on it, like so

paintTheSky("violet",1000)
  .then()
Enter fullscreen mode Exit fullscreen mode


and in this then() we can add a callback for paintTheSky().

 

paintTheSky("violet",1000)
  .then(()=>{
      paintTheSky("indigo",1000);
});
Enter fullscreen mode Exit fullscreen mode


If we add a return to the paintTheSky() called in the above fragment, we will effectively be returning a promise from .then(). So,

paintTheSky("violet",1000)
  .then(()=>{
      return paintTheSky("indigo",1000);
});
Enter fullscreen mode Exit fullscreen mode


the code above is same as

 

paintTheSky("violet",1000)
  .then(()=> paintTheSky("indigo",1000)); /*using implicit return*/
Enter fullscreen mode Exit fullscreen mode


Since we want more colours of the rainbow to be printed, we can simply chain more .then() for the purpose, effectively achieving the same result as we did above, when we used callbacks only.

const paintTheSky = (colour, timeInterval) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      document.body.style.backgroundColor = colour;
      resolve();
    }, timeInterval);
  });
};

paintTheSky("violet", 1000)
  .then(() => paintTheSky("indigo", 1000))
  .then(() => paintTheSky("blue", 1000))
  .then(() => paintTheSky("green", 1000))
  .then(() => paintTheSky("yellow", 1000))
  .then(() => paintTheSky("orange", 1000))
  .then(() => paintTheSky("red", 1000));
Enter fullscreen mode Exit fullscreen mode


In the above code we are calling paintTheSky() and then since its returns a promise, we are using .then() to call the it again and then again and again. We can immediately see that not only have we flattened out the code structure, it has become very easy to read as well.

This was easy. Why? we didn't reject the promise, there was no reason to. However, there will be situations when we might need to reject it as well. In the DC callback hell example above, we made provisions for what to do if the (fake) API responded in time and otherwise.

To rewrite that example, using promises, we will first rewrite the requestDataFromAPI(), like so,

const requestDataFromAPI = (url) => {
  return new Promise((resolve, reject) => {
    const timeInterval = Math.floor(Math.random() * 4000) + 500;
    setTimeout(() => {
      if (timeInterval > 3000) {
        reject(`Couldn't get the data.`);
      } else {
        resolve(`Data Retrieved: ${url}`);
      }
    }, timeInterval);
  });
};
Enter fullscreen mode Exit fullscreen mode


The requestDataFromAPI() above takes in one parameter, the url (though we don't do anything with it, fake api request remember 😅). Inside the function, we are creating a new promise which takes in two parameters. First one is a callback which executes when the promise is resolved and the second is a callback which executes when the promise is rejected. Depending upon the if condition, that evaluates the comparison between timeInterval value and 3000, we either resolve the promise or reject it.

const requestDataFromAPI = (url) => {
  return new Promise((resolve, reject) => {
    const timeInterval = Math.floor(Math.random() * 4000) + 500;
    setTimeout(() => {
      if (timeInterval > 3000) {
        reject(`Couldn't get the data.`);
      } else {
        resolve(`Data Retrieved: ${url}`);
      }
    }, timeInterval);
  });
};

requestDataFromAPI("dccomics.com/trinity") //returns a thenable object
  /* if promise gets resolved, then() is executed.*/  
  .then(() => {
    console.log("DC Trinity: Superman, Wonder Woman, Batman");
  })
  /* if  promise gets rejected, catch() is executed */
  .catch(() => {
    console.log("Trinity is gone, all hail Darkseid!.");
  });
Enter fullscreen mode Exit fullscreen mode


Since we have to either resolve or reject on each API endpoint call, we won't be able to chain the promises (we will see how can we chain the promises for this example and at what cost, a bit later). After adding code for the remaining two endpoints, the complete code looks like this

const requestDataFromAPI = (url) => {
  return new Promise((resolve, reject) => {
    const timeInterval = Math.floor(Math.random() * 4000) + 500;
    setTimeout(() => {
      if (timeInterval > 3000) {
        reject(`Couldn't get the data.`);
      } else {
        resolve(`Data Retrieved: ${url}`);
      }
    }, timeInterval);
  });
};

requestDataFromAPI("dccomics.com/trinity") //returns a thenable object
  .then(() => {
    console.log("DC Trinity: Superman, Wonder Woman, Batman");
  })
  .catch(() => {
    console.log("Trinity is gone, all hail Darkseid!.");
  });

requestDataFromAPI("dccomics.com/citizensOfThemyscira")
  .then(() => {
    console.log(
      "Citizens of Themyscira: Queen Hippolyta, Artemis, Cassie Wonder Girl, Donna Troy"
    );
  })
  .catch(() => {
    console.log("Alas!, Ares won and took over Themyscira");
  });

requestDataFromAPI("dccomics.com//greenlanterncorps")
  .then(() => {
    console.log(
      "Members of Green Lantern Corps; John Stewart, Jessica Cruz, Hal Jordan, AbinSur, Alan Scott, Guy Gardener"
    );
  })
  .catch(() => {
    console.log(
      "Oh No!, Sinestro has brought the green lantern corps under his control of fear."
    );
  });
Enter fullscreen mode Exit fullscreen mode


and of course, we can further update it to this

const requestDataFromAPI = (url) => {
  return new Promise((resolve, reject) => {
    const timeInterval = Math.floor(Math.random() * 4000) + 500;
    setTimeout(() => {
      if (timeInterval > 3000) {
        reject(`Couldn't get the data.`);
      } else {
        resolve(`Data Retrieved: ${url}`);
      }
    }, timeInterval);
  });
};

requestDataFromAPI("dccomics.com/trinity") 
  .then(() => console.log("DC Trinity: Superman, Wonder Woman, Batman"))
  .catch(() => console.log("Trinity is gone, all hail Darkseid!."));

requestDataFromAPI("dccomics.com/citizensOfThemyscira")
  .then(() => console.log("Citizens of Themyscira: Queen Hippolyta, Artemis, Cassie Wonder Girl, Donna Troy"))
  .catch(() => console.log("Alas!, Ares won and took over Themyscira"));

requestDataFromAPI("dccomics.com//greenlanterncorps")
  .then(() => console.log("Members of Green Lantern Corps; John Stewart, Jessica Cruz, Hal Jordan, AbinSur, Alan Scott, Guy Gardener"))
  .catch(() => console.log("Oh No!, Sinestro has brought the green lantern corps under his control of fear."));
Enter fullscreen mode Exit fullscreen mode


In the above code, we see how easy, clean and maintainable our code has become. 

From a QA's point of view, we would, for most of our time be dealing with standard libraries (and the promises returned by their methods) like playwright, supertest, jest etc., and may never (or seldom) get to write ourselves something that creates and returns promises.
Therefore, instead of focusing on implementation of requestDataFromAPI(), we should be focusing on its usage. BUT then…, this is me, I leave the choice to you 😇

However, we can do the same stuff in a much neater way. Enter async-await 😇


async await

We will again read the MDN docs to learn what asyncand await are

The async function declaration declares an async function where the await keyword is permitted within the function body. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

In simpler words, what this means is that async and await are essentially syntactical sugar wrapped around promises to make them look easier. All we need to do is to put all of our asynchronous code inside a function, an async function. async functions allow us to write asynchronous code in a synchronous manner. How? 

The async keyword allows the function to return a promise which would either get resolved or rejected, depending upon what we are doing inside that async function. That is, if the function returns a value, promise is said to be resolved with the returned value and if it throws an exception then it is said to be rejected with the exception value. Now, inside the async function, we write asynchronous statements using the keyword await. This keyword pauses the execution of the async function and waits for the promise to be resolved.

Consider the rainbow example, we can transform the promise based implementation to async-await, like this:

const paintTheSky = (colour, timeInterval) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      document.body.style.backgroundColor = colour;
      resolve();
    }, timeInterval);
  });
};

const paintTheRainbow = async function () {
  await paintTheSky("violet", 1000);
  await paintTheSky("indigo", 1000);
  await paintTheSky("blue", 1000);
  await paintTheSky("green", 1000);
  await paintTheSky("yellow", 1000);
  await paintTheSky("orange", 1000);
  await paintTheSky("red", 1000);
};
paintTheRainbow();
Enter fullscreen mode Exit fullscreen mode


In the example above, paintTheRainbow is an async function. Inside this function, we have multiple await statements in the order in which we want the colour transition to happen (same as in the callback and promise examples). The first await statement calls the paintTheSky() function and waits for the promise returned by the function to be resolved. After that, the next await statement is executed and after its promise has been resolved, the next one and so on. 

We see that the same asynchronous code that we wrote using the callbacks (which had multi level nesting, thus the phrase 'callback hell') and the promises (which had atleast one level of nesting) has been written as if we are writing synchronous code. That's the beauty of async-await.
When we use JavaScript for automation (frontend/ backend), in later tutorials, we shall write almost all of our tests (and most of the helpers/ utility functions) using async-await.

We can convert the promise version of DC example also into its corresponding async-await implementation, where we will observe that handling of promise rejection in case async- await is pretty similar to the way it was in the promise example above (async-await is just syntactic sugar on promises, remember 😅).

const dcUniverse = async () => {
  try {
    await requestDataFromAPI("dccomics.com/trinity");
    console.log("DC Trinity: Superman, Wonder Woman, Batman");
  } catch (error) {
    console.log(`${error} Trinity is gone, all hail Darkseid!.`);
  }

  try {
    await requestDataFromAPI("dccomics.com/citizensOfThemyscira");
    console.log(
      "Citizens of Themyscira: Queen Hippolyta, Artemis, Cassie Wonder Girl, Donna Troy"
    );
  } catch (error) {
    console.log(`${error} Alas!, Ares won and took over Themyscira`);
  }

  try {
    await requestDataFromAPI("dccomics.com/greenlanterncorps");
    console.log(
      "Members of Green Lantern Corps; John Stewart, Jessica Cruz, Hal Jordan, AbinSur, Alan Scott, Guy Gardener"
    );
  } catch (error) {
    console.log(
      `${error} Oh No!, Sinestro has brought the green lantern corps under his control of fear.`
    );
  } finally {
    console.log("Heroes shall rise again.");
  }
};

dcUniverse();
Enter fullscreen mode Exit fullscreen mode

Outro

In this part of the tutorial, we covered the asynchronous javascript concepts including common patterns used for the purpose. The knowledge of JavaScript fundamentals, we acquired from the tutorial should help us immensely when we take our plunge in automation testing, both frontend and backend.

As always, please do share your thoughts and feedback on this tutorial. I will surely make amends where ever necessary and it will help me improve.

💖 💪 🙅 🚩
asheeshmisra
Asheesh Misra

Posted on October 15, 2023

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

Sign up to receive the latest update from our blog.

Related