Writing Code in JavaScript in Asynchronous Way

Believe it or not, JavaScript is, at its core, a synchronous, blocking, single-threaded language. This means that when it fetches data from a remote network, it blocks the rest of the code until the request is completed. This is how JavaScript was designed to run.

However, over the years, it has evolved significantly, to the point where it can now behave like an asynchronous, non-blocking language. In this article, we will discuss how JavaScript became asynchronous with the help of setTimeout, Promise, and async/await.

Table of Contents

  1. Callbacks
  2. Promise
  3. Async Await

Call Backs

We all know that JavaScript executes lines of code in an orderly fashion. However, when we encounter a task that might take some time to process, the rest of the code will hang until the task is completed.

The simplest and oldest way to solve this problem is to use callbacks. A callback is a type of function that can be passed as an argument to another function. Let's explore callbacks with an example-

function add(a,b){
    return a b
}


function compute(callBackFuction, x, y){
    return callBackcallBackFuction(x,y)
}

console.log(compute(add, 10, 5)) //15

In this example, when we call the compute function, we pass a callback function (in this case, it is the add function) which will be invoked inside the compute function.

So, how can we leverage this to make our code asynchronous? Have you ever heard of something called setTimeout in JavaScript? setTimeout is a built-in function in JavaScript that takes two parameters: a callback function and the number of milliseconds to wait before executing the callback.

console.log("before")

function printHello(){
    console.log('middle')
}

setTimeout(printHello, 2000)

console.log("after")

If we run this code, we will notice that the console logs "middle" at the end. In the setTimeout function, we use printHello() to wait for 2 seconds. Since setTimeout is asynchronous by nature, it doesn't hold the rest of the code. So, "console.log('after')" executes after "console.log('before')". While callbacks are a basic way to handle asynchronous operations, they are not the most efficient way.

Imagine a situation where we need to use multiple callbacks in many levels. This can lead to a problem called "callback hell". It may become difficult to manage and understand the code later. Can we avoid callback hell? Promises can be used to solve this problem. Let's explore how-

Promise

Promises provide greater flexibility for performing asynchronous operations in JavaScript. A promise is an object that represents the outcome of an asynchronous operation, which can be either fulfilled (resolved) or rejected.

We can compare it to a game where there are two possible outcomes: winning or losing. Similarly, when we request data over a network, we can either receive the data (resolved promise) or fail to receive it (rejected promise).

let game = new Promise(function(resolve, reject){
    console.log("A Game is happening");

      
    setTimeout(() => {    
        if(Math.random() >= 0.5){
            resolve("You Won!!!")
        }
        else{
            reject(new Error("You Lost"))
        }   
    }, 5000);  
    
}).then((response) =>{
    console.log(response);
}).catch((err) =>{
    console.log(err);
})

In the above example, we create a promise and return its state based on the comparison. Since promises are asynchronous, we cannot access the result directly. Therefore, we use the then() method to access the result.

The main advantage of using promises is that we can chain them, similar to what we did in callback hell. However, this time it provides much better readability and control over the code. Let's see how.

function delay() {
    return new Promise((resolve) => {
        setTimeout(resolve, 1000);
    });
}
delay().then(() => {
    console.log('Step 1');
    return delay();
})
.then(() => {
    console.log('Step 2');
    return delay();
})
.then(() => console.log('Step 3'));

Async Await

Finally, let's talk about Async Await. While promises provide flexibility for handling asynchronous operations more efficiently, the problem of callback hell still persists. Whenever we need to handle multiple asynchronous operations, we must do it by chaining promises with .then(), which can make the code less readable. That's why in ES6, we got Async Await.

Under the hood, Async Await works with promises, but it provides superior syntactical advantages. It is easy to use - we just need to add the async keyword before a function and use await when assigning resolved data to a new variable or calling another async function. Let's take a closer look at the syntax with the following example.

const asyncFunction = async() => {
		const response = await fetch(resource);
   	const data = await response.json();
}

Now its time for realistic example-

const webSiteName = new Promise((resolve) =>{
     return setTimeout(() => resolve('shouts.dev'), 1000)
})
const webSiteType = new Promise((resolve) =>{
     return setTimeout(() => resolve('blogging'), 2000)
})

const getMyDetails = async() => {
    const name = await webSiteName;
    const type = await webSiteType;
        console.log(`Our Website name is ${name} and our site type is ${type}`)
}

getMyDetails()

In the above example, we create two promises and call them asynchronously inside the async function. We can chain as many promises as we want using async/await, without having to use .then(). This makes it much more convenient for handling multiple promises chaining.