top of page

In Sync with JavaScript: Synchronous vs. Asynchronous

When learning JavaScript, we regularly hear that it is a single-threaded and an asynchronous programming language. These terms challenge one another, which begs the question: how can a programming language be single-threaded and asynchronous simultaneously? At the same time, however, we often move forward and spend too little time fully grasping this important concept of JavaScript. As we learn more about this very popular programming language and start writing our first applications, we quickly realize how important it is to gain a solid understanding of JavaScript’s order of function execution. Since new and experienced coders often catch themselves resolving issues that arose because of an incorrect order of operations, we decided to explain in more detail how asynchronous JavaScript works and how to take advantage of this important characteristic.


In Sync with Synchronous JavaScript

Synchronous programming languages execute code in a sequence or an order that often goes from top to bottom. JavaScript will execute code exactly in that way – top to bottom. For a simple example, take a look at the code below:


let a = "First"; 
let b = "Second"; 
console.log(a);
console.log(b);

JavaScript will start by executing the first line of code (console.log(a)) and then move the second line (console.log(b)). This is what synchronous means – the code executes in a sequence and, as a result, we receive the following output in our Console:


Figure 1. Console Output Associated with Figure 1

The same principle applies when we write functions and more complicated pieces of JavaScript logic. The programming language will execute everything line-by-line and it will use a call stack to keep track of the function execution order. For example, let’s assume we have the following functions:


function f1() {
    console.log(1)
}
function f2() {
    f1()
    console.log(2)
}
function f3() {
    f2()
    console.log(3)
}

f3();

When invoking function f3, JavaScript will create a call stack and place that function on the very bottom. Next it will read the body of function f3 and realize that f2 has been evoked so it will place that function in a call stack on top of f3. Similarly, when reading the body of function f2, JavaScript will realize the need to add function f1 to the top of the call stack, resulting in the following order of operations and a corresponding console output:

Figure 2. Call Stack and Console Output Associated with Figure 3.

Can JavaScript Get Out of Sync?

Although JavaScript executes code synchronously, we describe this programming language as asynchronous because it allows for multiple ways of executing its code out of the previously described top-to-bottom sequence. The two main methods to do so are:

  • Browser/web API events and functions (such as the setTimeout function or the onClick event)

  • Promises (such as those resulting from a fetch request)

Browser and web API events, as well as event handlers, require a callback function that will execute after a completed asynchronous operation. For example, the setTimeout function will execute after a specified amount of time passes. Similarly, onClick functions will only execute if the user clicks in the designated area on the page. The callback function is stored in the callback queue, also called the task queue. Therefore, JavaScript will keep executing code in the stack as usual. If there is a callback function in the queue, that function will be pulled from the task queue and placed in the stack for execution. This can be seen in the below example:


function f1() {
    console.log(1)
}
function f2() {
    console.log(2)
}
function f3() {
    console.log(3);
    setTimeout(f1, 2000);
    f2();
}

f3();

In this case, the code will execute in the following order:

Figure 3. Call Stack and Task Queue Associated with Figure 5. Code

The resulting console output will be as follows:

Figure 4. Console Output Associated with Figure 5. Code

One thing to note is that the number of milliseconds in the setTimeout function does not actually matter. In the above example, the number of seconds could be changed from 2000 to 0 and the output would still be the same as in Figure 7. That is because the setTimeout function changes the code to asynchronous and therefore removes it from the usual call stack. The code then is added to the task queue and runs only once the call stack is emptied.


In addition to Browser API, JavaScript uses promises to perform asynchronous operations. A promise is an object that only passes if certain criteria are true. With promises, JavaScript will delay a code execution until an asynchronous request is completed. A promise has three stages:

  1. Pending: an initial state before the promise passes or fails

  2. Resolved: a successful promise

  3. Rejected: a promise that failed

Once a promise is executed, we can use the .then() method to handle its results and the .catch() method to manipulate any errors. One of the most common ways to use promises is the fetch() method used to get data from the API (see example below).


fetch('url')
    .then(response => {
        console.log(response);
    })
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log(error)
    });

It is important to note that JavaScript does not use a previously mentioned callback queue when dealing with promises. Instead, the executor goes to the job queue every time it encounters a promise. The order of operations when it comes to the job queue is the same as with the callback queue, but the priority is given to the job queue over any items in the callback queue (see below for more details).



function f1() {
    console.log('1');
}

function f2() {
    console.log('2');
}

function f3() {
    console.log('3');
    
    setTimout(f1, 2000);
    
    new Promise((resolve, reject) => 
        resolve('I am a promise')
        )
        .then(resolve => console.log(resolve))
        
        f2();
}

f3();

The code above will result in the creation of both, a callback queue and a job queue as shown in in the below graph:

Figure 5. Call Stack, Task Queue, and Job Queue Associated with Figure 9. Code

The console output associated with our code will be as follows:

Figure 6. Console Output Associated with Figure 9. Code

Case Study: Synchronous Calls for Asynchronous Code


Now that we know how to use synchronous and asynchronous JavaScript, let’s take it a step further by examining a case we encountered when working on one of Boulevard’s applications.


For this specific application, our team wanted the user to confirm their action to delete a contract with a message box similar to the following:

Figure 7. Sample Pop-Up Box Confirming User’s Intention to Delete

When the user selected the “Delete” option, our intention was to show them a confirmation of a successful action:

Figure 8. Sample Pop-Up Box Confirming Contract Deletion

To accomplish the above tasks, we used dialog boxes activated with an onClick event. Once the user clicked the initial “Delete” button, the onClick event would change variable “open” from False to True (with setOpen(true)). This would open the dialog box shown in Figure 12. The action would also add shadow to everything except for the message box and it would disable scrolling – a standard procedure used in order to direct users’ attention to the message box. Once the user confirmed the intention to delete a contract, the onClick event would change the open variable back to False at the same time as it would change the deleteOpen variable controlling the box in Figure 13 to True, as shown in Figure 14 below:


setOpen(false);
setDeleteOpen(true);

These two lines of code seem simple and straightforward, but unfortunately, they created an issue where some of the dialog box settings (scrolling ability) did not return to original after the user closed both messages. That is because both functions were synchronous, executing top-to-bottom and now allowing the system to return to original window settings before moving forward with changing the settings associated with the second box (Figure 13.). This mistake was hard to detect, and it risked the possibility of greatly impairing the user experience. The solution, however, was simple. The second function just needed to be asynchronous and run after the call stack of all remaining functions emptied. A simple setTimeout function accomplished this task:


setOpen(false);
setTimeout(() => {
    setDeleteOpen(true);
}, 0);

It is interesting to note that we delayed the setDeleteOpen function by 0 seconds. We did not have to set an actual delay by adding anything greater than 0 seconds. Our goal here was not to delay the code but to change its order of execution. Therefore, we took advantage of the order of execution shown in Figure 6 and we used the setTimeout method to change JavaScript from synchronous to asynchronous and to have the setDeleteOpen function run at the end.


Proper understanding of JavaScript’s synchronous and asynchronous capabilities is necessary in order to build robust and high-performing applications. At the same time, errors associated with incorrect function execution can be one of the most difficult to detect and debug. Open-sourced tools are available to find some solutions to your problem at the beginning stages of building your application. As your project progresses, however, we guarantee that no website will be able to resolve an issue that originated from improper use of synchronous and asynchronous functions. The best way of resolving such bugs, or to avoid them whatsoever, is to devote the time to understanding JavaScript’s order of code execution. Our team of data experts are always available to have discussions to further the understanding of JavaScript to improve applications at marketing@boulevardcg.com


References

Join us as we explore the future of business through the lens of machine learning & AI, data-driven analytics, cloud computing, and operational transformation.

OUR EXPERTISE

GET MORE IN DEPTH 

INSIGHTS PORTAL

THE ROAD TO BETTER DATA

bottom of page