Understanding Asynchronous JavaScript

Asynchronous JavaScript

The term “asynchronous” is often associated with JavaScript. Asynchronous is a confusing concept, especially for beginners. Programmers fail to understand what is asynchronous programming and how it is associated with JavaScript. It becomes more confusing when they realize JavaScript is actually synchronous, not asynchronous. So how can we relate JavaScript with asynchronous? As mentioned above, asynchronous is confusing and it is even more confusing when related to a programming language like JavaScript. So in this article, we will dive deep into asynchronous programming in JavaScript.

First of all, we need to understand the difference between synchronous and asynchronous programming. In simple words, executing one thing at a time is called synchronous while executing multiple things at a time is called asynchronous.

JavaScript is synchronous, single-threaded, and blocking language. So it is synchronous, meaning, it can execute one statement at a time. It is single-threaded, meaning, it has a single “call stack”. We will discuss blocking later. Let’s understand how JavaScript code executes. Observe the following JavaScript code.

function fun(){
    console.log("Executed in the function");
}

console.log("Executed first...");

fun();

console.log("Executed last...");

It is a very simple code. But we need to dive deep in it, and for that, we need to understand the meaning of execution context and call stack.

An environment where the JavaScript code is evaluated and executed is called the Execution context. There are three types of execution context – Global Execution Context (GEC), Functional Execution Context (FEC), and Eval. All the execution contexts are stored in a stack called the call stack.

There is only one call stack in JavaScript because it is a single-threaded programming language. The call stack is based on LIFO, meaning, Last In First Out. Now, let’s understand the execution of the above code.

1. When the code is executed, a global execution context is created which is the first entry in the call stack.

main() ->

2. Then, console.log(“Executed first…”) is pushed into the stack.

main() -> console.log("Executed first...")

3. After its execution is completed, it is popped out of the stack.

main() ->

4. After that, fun() is pushed into the stack.

main() -> fun() ->

5. Now, fun() is executed. console.log(“Executed in the function”) is pushed in the stack.

main() -> fun() -> console.log("Executed in the function") ->

6. After the console statement is executed, it is poped out.

main() -> fun() ->

7. Function execution ends and fun() is popped out.

main() ->

8. After that console.log(“Executed last…”) is pushed into the stack.

main() -> console.log("Executed last...") ->

9. console.log(“Executed last…”) executes and is poped out.

main() 

10. The program execution is completed, so the main() is also removed from the stack.

The purpose of the entire above demonstration was to make you understand how JavaScript executes the code line by line and it only moves to the next line when the line being executed has finished its execution. This is synchronous JavaScript.

In the example, nothing was being blocked. But there can be such scenarios when the call stack is blocked due to some reasons. For example, calling APIs can block the call stack. The network requests generally take some time, thus blocking the call stack. Observe the following code.

function APICall(url){
    // some operation
}

APICall(url)

console.log("Execution ends here...");

Suppose the “APIcall” function is calling an API. So what happens here? First, the APICall function completes the execution and then, console.log(“Execution ends here…”) statement is executed. But as the APICall function is making an API call, it can take some time. So, the execution is blocked at this point and the console statement will not be executed until the execution of APICall is not completed. Here comes the concept of asynchronous programming.

The execution must not stop. It should not wait for the APICall function to complete the execution, instead it should move further, executing the console statement. This is asynchronous.

Callback function

The solution to the above problem is callback functions. JavaScript functions are first-class functions. A JavaScript function can be assigned to a variable, passed to another function as a parameter, and also it can be returned from another function. You can learn more about this here.

A function passed to another function as an argument is called a callback function. Let’s understand with the help of a simple example.

console.log("Executed first...");

setTimeout(() => {
    console.log("Executed after 5 seconds...");
  }, 5000);

console.log("Executed last...");

What do you think is the output of the above code? Normally, console.log(“Executed first…”) should be executed first, then the setTimeout function, and at last console.log(“Executed last…”). But the above code is not synchronous, it is asynchronous. Observe the setTimeout function.

setTimeout(() => {
    console.log("Executed after 5 seconds...");
}, 5000);

The first parameter of the setTimeout function is a callback function and the second parameter is 5000, meaning 5 seconds. So as a whole, it means, the callback function will be executed after 5 seconds. If you check the whole code again, you will see a console statement after the setTimeout function. Now, what will happen here? The output of the code is:

  • Executed first…
  • Executed last…
  • Executed after 5 seconds…

Yes, the execution will not wait for 5 seconds, instead it will move next and execute the console statement, i.e. console.log(“Executed last…”) and only after 5 seconds, console.log(“Executed after 5b seconds…”) is executed. This is not synchronous and blocking, instead it is asynchronous and non-blocking.

The setTimeout function waits for 5 seconds while the execution moves to the next line. After 5 seconds, the callback function of the setTimeout function is executed. This is how the callback function prevents blocking and helps in asynchronous programming.

Let’s understand what is happening in the call stack.

1. console.log(“Executed first…”) is pushed into the stack.

main() ->; console.log("Executed first...")

2. After its execution, the console statement is popped out and the setTimeout function is pushed into the stack.

main() -> setTimeout()

The setTimeout function executes. It will wait for 5 seconds and then execute the callback function. This function is a part of the browser’s web APIs. The timer is pushed in the web API environment.

3. At the same time, the execution of the setTimeout function has ended and it is popped out of the stack while the next statement, i.e. console.log(“Executed last…”) is pushed in the stack.

main() -> console.log("Executed last...")

4. After the console statement is executed, it is popped out of the stack.

At this point, the output is:

  • Executed first…
  • Executed last…

Meanwhile, the timer is still going on in the web API environment and when it ends, the callback function is pushed into the “message queue”.

5. At the point when the timer expires, the “event loop” checks the message queue if any callback exists there or not. In our case, one callback is present in the message queue. Thus, it is pushed in the call stack.

main() -> callback()

6. The callback executes.

main() -> callback() -> console.log("Executed after 5 seconds...")

7. After the console statement is executed, it is popped out along with the callback function.

main()

8. In the end, the execution is completed and we have the following output.

  • Executed first…
  • Executed last…
  • Executed after 5 seconds…

We have two important terms here – message queue and event loop.

  • Message queue – The callbacks are held here.
  • Event loop – It checks if any callback is present in the message queue or not.

Let’s create a callback function manually.

function add(x, y, callback){
    var z = x + y;
    callback(z);
}

add(10, 20, function(z){
    console.log("Double of z:", z * 2)
})

The “add” function has three arguments – x, y, and a callback function. The sum of x and y is calculated in the add function and then, it is passed to the callback function. We can also write the above code as follows.

function double(z){
    console.log("Double of z:", z * 2)
}

function add(x, y, callback){
	var z = x + y;
	callback(z);

}

add(10, 20, double);

Instead of passing an anonymous function, we can simply create a separate function.

Callback hell

Using callback functions is a way to handle asynchronous. But using too many callback functions can result in callback hell.

Callback hell is a situation when too many nested callbacks are used. Observe the following code.

const userVerification = function(uname, pword, callback){
    db.userVerification(uname, pword, (err, info) =>{
        if(err){
            callback(err)
        }
	else{	
	    db.fetchRoles(uname, (err, roles) =>{	
	        if(err) {
		    callback(err)
		}
		else{
		    callback(null, info, roles)
		}
            }
	}
   
    }
}

The code is complicated. It will result in callback hell. First, the “userVerification” method is called for “info” and the “fetchRoles” method is called for “roles”. As these are database operations, they can take some time, meaning this is asynchronous. Now to make it more complex, say, there are more callbacks in “userVerification” and “fetchRoles” functions.

So many nested callbacks will cause a problem. So to avoid the callback hell, we have two solutions – promise and async/await.

Promise

Do you make promises in real life? Promises in real life are either fulfilled or broken. Promises in JavaScript work quite similarly. Let’s understand with the help of a simple example.

Suppose, Sam’s birthday is on 10th January, and today is 1st January. Sam’s father made a promise to him. He will buy Sam a brand new iPhone on his birthday which is 10 days later. From 1st January to 9th January, the promise made by Sam’s father is pending. Only on 10th January, Sam will find out what happened to the promise that his father made. There can be two scenarios – Sam’s father fulfills the promise and Sam gets a brand new iPhone or Sam’s father breaks the promise.

The promises work similarly in JavaScript. A promise in JavaScript has three possible states – pending, fulfilled, and rejected. Let’s create a promise.

const promise = new Promise((resolve, reject) => {

})

A promise is created using the new keyword and “Promise” method. The Promise method has a callback as its parameter. The callback function, in turn, has two parameters – resolve and reject. Let’s understand with a working example.

Resolving a promise

Suppose, we have three functions in a program – add, double, and display.

The add function returns the sum of two numbers in 5 seconds, the double function returns the double value of the sum in 3 seconds, and the display function returns the final message in 2 seconds. This is asynchronous but can we use callback functions in such a program? No! We have to use promises.

Let’s start piece by piece to understand properly

const add = (x, y) =>{
    return new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(x + y)
        }, 5000 )
    })		
}

console.log(add(10, 20));

The add function returns a promise. This promise has a setTimeout function. Let’s see what is printed in the console.

Promise
const add = (x, y) =>{
    console.log("Wait for 5 seconds...")

    
    return new Promise((resolve,reject) => 
        setTimeout(() =>{
            resolve(x + y)
        }, 5000 )
    })		
}

add(10, 20).then( value => {
	console.log(value);
})

“then” method is attached with the add function, and this method also has a callback function as the parameter. What do you think is happening here?

The promise returned by the add function is pending. When the event finishes, meaning, the timer of the setTimeout function ends, the promise is resolved. The then method is used to handle the promise. Let’s execute the code.

Promise wait

The parameter of the then method is nothing but the value we passed to the resolve function earlier in the setTimeout function.

const display = (q) =>{
    console.log("Wait for 2 seconds...")
    return new Promise((resolve,reject) => {

        setTimeout(() =>{
            resolve("The final value is: " + q)
        }, 2000 )
    })		
}

const double = (z) =>{
    console.log("Wait for 3 seconds...")
    return new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(z * 2)
        }, 3000 )
    })		
}

const add = (x, y) =>{
    console.log("Wait for 5 seconds...")
    return new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(x + y)
        }, 5000 )	
    })		
}

add(10, 20).then( value1 => {
    double(value1).then( value2 =>{

        display(value2).then( value3 =>{
            console.log(value3)
        })
    })
	
})

Above is the final code of the example we discussed earlier. It is complicated. So we will discuss piece by piece.

add(10, 20).then( value1 => {

In this line, the add function is called and it returns the sum of 10 and 20 in 5 seconds. “value1” is the sum of 10 and 20.

add(10, 20).then( value1 => { 
    double(value1).then( value2 =>{

After waiting for 5 minutes, we have the sum, so we passed it to the double function. “value2” is returned by the double function after 3 seconds.

add(10, 20).then( value1 => { 
    double(value1).then( value2 =>{ 
        display(value2).then( value3 =>{

After waiting for 2 seconds, we have the double value, so we passed it to the display function. “value3” is returned by the display function after 2 seconds.

add(10, 20).then( value1 => {
    double(value1).then( value2 =>{
        display(value2).then( value3 =>{
            console.log(value3)

        })

    })
})

In the end, “value3” is printed in the console.

Promise 3

Rejecting a promise

Now, suppose, we do not want to add the number if they are equal. We have to reject the promise. To reject a promise, we can attach a “catch” method.

const add = (x, y) =>{

    return new Promise((resolve,reject) => {
        if(x != y){
            setTimeout(() =>{
                resolve(x + y)
            }, 5000 )
        }
        else{
            reject("x and y are equal!")
        }
    })		
}

add(10, 10).then( value1 => {
    console.log(value1);
})
.catch(err => {
    console.log(err);
})

The promise is rejected when x is equal to y. So we have a catch method attached after then. The catch method has a callback function as its parameter and the callback function, in turn, has a parameter, i.e. error which is displayed in the console.

Promise 4

So this is the concept of promise in JavaScript.

Async/await

Working with promises is complicated. In the real application, you will be handling multiple API calls, and there will be several mechanisms to work with the data. The example we discussed above is simple and easy, but in real applications, things will be complicated and complex. Even the experienced developers struggle while using promises. So the concept of async/await was revealed with ECMAScript 2017.

The async/await is an approach to use promises in a better and easy way. These are nothing but keywords that help us synchronously write promise-based code, but without blocking the main thread. In simple words, we can write asynchronous code as it was synchronous.

A function declared with the “async” keyword is an async function that returns a promise. It does not matter what an async function is returning, it will always be a promise. Observe the following code.

async function add(x, y){
    return x + y
}

console.log(add(10,20))

“add” is an async function. It returns the sum of x and y. Let’s check the console.

Async 1

It returns a promise. But we only return the sum of x and y, right? This is the magic of async. Now, we can simply use the “then” method to unwrap the promise.

async function add(x, y){
    return x + y;	
}

add(10,20).then( value => {
    console.log(value); //30
})

We can clearly see the difference async function can make.

Let’s talk about “await” now. But first, observe the “add” function.

const add = (x, y) =>{

    console.log("Wait for 5 seconds...")

    return new Promise((resolve,reject) => {

    
    setTimeout(() =>{

    
    
    resolve(x + y)

    
    }, 5000 )
	

    })		
}

add(10, 20).then( value => {

    console.log(value);
})

We will modify the above code using async/await.

The “await” keyword will help us reduce the complexity of the above code. This keyword holds the JavaScript execution until the promise is settled.

function add(x, y){
    const pr = new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(x + y)
        }, 5000 )
    })	

    const sum = await pr; 

    console.log("Sum of x and y:", sum);
}

In this function. a promise (pr) is created. Observe the next line.

const sum = await pr;

The execution will halt at this point without blocking the main thread. But we cannot use “await” in a normal function. It should be an async function. And the best is that we do not need the “then” method to unwrap the promise. The “await” keyword handles everything.

async function add(x, y){
    const pr = new Promise((resolve,reject) => {
        setTimeout(() =>{
            resolve(x + y)
	}, 5000 )
    })	
	
    console.log("Waiting for 5 seconds...");
    const sum = await pr; 
    console.log("Sum of x and y:", sum);
}

add(10, 20)

Let’s execute the code.

Async 2

Perfect! So this is how we can use async/await as the alternative of the traditional promise approach.

Wrapping it up

In this article, we covered everything regarding asynchronous programming, starting from synchronous and asynchronous programming, how to handle asynchronous using callback, what is callback hell and how it encounters it using promises, and finally, the alternative to promises – async/await.

If you want to become a JavaScript developer, and you think you are going to work with node.js, then understanding asynchronous in JavaScript is very important. First, we discussed how JavaScript is synchronous and asynchronous. We went deep understanding the execution context, call stack, message queue, and event loop. People generally skip these topics and it is the main reason why they fail to understand asynchronous in JavaScript.

The first approach to handle asynchronous JavaScript is by using callback functions. It is a simple and useful concept. But as the callback function increases and there are more and more nested callback functions, we end up in a situation called callback hell. To encounter callback hell, we have JavaScript promises.

The promises can handle callback hell efficiently but as the program grows, the code becomes more and more complex. So to make it easier, ECMAScript 2017 brought the concept of async/await. So this is all about asynchronous JavaScript, callback functions, promises, and async/await. No doubt, these concepts are a bit complicated at first, but it is necessary to understand them.

Image Credits: Photo by Dawit on Unsplash.

More Similar Posts

Menu