JavaScript Specifics – Asynchronicity

Many of us, who work (or worked) with JavaScript (JS) tool stack, could have faced some strange behavior during tests automation. We even could spend hours and hours debugging the problem and figuring out what is wrong with our code. Basing on the experience, the most common reason of those troubles is the lack of knowledge of the JavaScript basics.

Despite the fact that JS seems to be easy to work with, there is a couple of very tricky places, you should know about before you start using it. One of the most important things is asynchronicity.

This article briefly describes what asynchronicity in JS is and how to handle it.

The Main Idea of Asynchronous Programming

Normally, a given program’s code runs straight along with only one thing happening at once. If a function relies on the result of another function, it has to wait for the other function to finish and return. Until that happens, the entire program is blocked.

Modern software design revolves around using asynchronous programming, to allow programs to do more than one thing at a time. This is the basis of asynchronous programming.

Let us dive into details – what is the problem here? Why do many of us face strange behavior of our code?

Sequence of Code Execution

JavaScript has both synchronous and asynchronous operations. Examples of synchronous operations are console outputs, math operations, data operations, type conversions, and others. An example of asynchronous operations could be HTTP requests, DB requests, timeout functions, and others. It begs the question, how they work together in conditions of a single JS thread?

In figure 1 below, we see that the thread is monopolized by the first request, and Node.js engine is not able to serve the second request until the synchronous operation finishes.

Single JS Thread

Fig. 1 Single JS thread

Despite the fact that JS is based on the hypothesis that synchronous operations are always short, and most of the operations are asynchronous, we should know how synchronous and asynchronous operations work together.

Problem Description

Let us look at a real example, which illustrates the problem that a Test Automation Engineer can face if he or she does not know how asynchronicity works. This example is implemented on Cypress.io/JS.

Test Automation Problem with asynchronicity

The method above checks if the checkbox has the attribute “checked.” If it does not – Cypress makes it selected. However, the method does not work as expected in case you need to call it twice. Why does it happen?

As you know, Cypress.io declares that it handles asynchronicity by itself, so you do not need to additionally use any async/await or Promises. Nevertheless, Cypress has both synchronous and asynchronous operations, and you cannot differentiate them in the code – they look quite similar.

In the example above Cypress.$(targetElement).length is synchronous, and cy.get(`checkbox[name=”${checkboxName}”]>label.checkbox-layout`).click()– is an asynchronous operation according to the official documentation. Basing on the way how asynchronicity works, Cypress checks the state of the checkbox at once, after that it sets calls of cy.get() and cy.click() to the event loop queue. And cy.get() and cy.click() functions’ callbacks will be executed on the event loop next tick (please see figure 2 below).

INFORMATION: The event loop is what allows JS to perform non-blocking operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system core whenever possible.

Asynchronicity schema

Fig. 2 Schema of the makeCheckboxSelected() method execution

As a result, Cypress checks the checkbox state twice before clicking on it, and then – clicks on the checkbox two times.

As you can see, the lack of knowledge can spell us trouble during tests implementation, because there are many similar traps caused by asynchronicity. Let us find out the ways to handle it.

Problem Solving

To correct the method behavior, we should make the flow wait until the checkbox is selected. Async/await cannot be used in the current case, because the Cypress methods do not return the Promise. However, an instance of Promise can be created directly in the target method:

Promisecreated directly in the target method

Thus, Cypress.$(targetElement) method always returns the correct checkbox state. The thread figure was changed in the following way:

Asynchronicity schema execution

Fig. 3 Schema of the makeCheckboxSelected() method execution (with Promise)

Let us figure out what the ways of asynchronicity handling exist in JS.

Asynchronicity Handling

There are three major ways to handle asynchronicity:

  • Callbacks;
  • Promises;
  • Async/await functions.

Callbacks

Callback is a function, passed as an argument to another function. Callback function is to be run after another function execution has finished.

For a long time, callback functions were the only way to handle async logic. Nevertheless, this approach had major problems and the biggest of them was the context loss and, consequently, very difficult error handling.

The following problems occurred during the work process:

  1. If async function did not provide error to callback, it was impossible to process it properly.
  2. There was also a so-called Callback hell problem, when we set callback in callback in callback, etc. With time, this code becomes impossible to maintain.

You can find the code examples and the schema of its work above.

Promises

Promise is a special JavaScript object, which represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Promises syntax fixed biggest callback problems. Promises brought plain readable calls, more or less convenient result pass to the next promises and, what is the most important, with promises we can catch all errors with .catch() promise method.

Despite the fact, that promises made handling async logic much simpler, they still have cons like using .catch() method instead of try/catch syntax or limited context pass.

Promise methods

Promises also brought a batch of methods, that help with processing of several async tasks simultaneously. They are:

  • Promise.all() – takes a collection of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises. This returned promise will resolve when all of the input’s promises have resolved, or if the input collection contains no promises. It rejects immediately upon any of the input promises rejecting or non-promises throwing an error, and will reject with this first rejection message / error. Promise.all() will reject as soon as one of the Promises in the array rejects.
  • Promise. allSettled() – takes a collection of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises. This returned promise will resolve when all of the input’s promises have resolved, or if the input collection contains no promises. It will never reject, it will resolve once all Promises in the array have either rejected or resolved.
  • Promise.any() – takes a collection of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise.
  • Promise.race() – returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise.

Async/await syntax

In fact, async/await syntax is a syntax sugar for the promises usage. Still, this syntax fixes most of promises problems. With async/await we have flat, sync-like code for async functions.

The only significant limitation of async/await approach is impossibility to achieve fluent interface for a class.

Conclusions

No matter what specific way we use to handle asynchronicity, whether we work with a tool that does it for us or we do it by ourselves. Anyway, we must know how asynchronous code works. Without this knowledge we would spend a lot of time debugging our solutions with no result.

Before using any JS tool, I recommend learning its documentation carefully to find out what methods (synchronous / asynchronous) the tool provides. This will help you understand the root cause of strange behavior, if it takes place, and resolve the problem much faster.

References

https://javascript.info/event-loop

https://javascript.info/microtask-queue

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Share article: