Asynchronous JavaScript

Abhishek ReddyAbhishek Reddy
8 min read

Call Backs :

What is a callback ? :

A callback is a function which is passed as an argument/parameter to another function.So the function which takes another function as an argument/parameter is known as Higher Order functions.

example:

function sumOfTwoNumbers(callback){
    let a = 10;
    let b = 20;
    let sum = a + b;
    callback(a,b,sum)
}

sumOfTwoNumbers((val1,val2,data)=>{
    console.log(`The sum of ${val1} and ${val2} is ${data}`};
});

From the above example we can see that the function sumOfTwoNumbers takes another function as a parameter and is called at a later part of time.So the function sumOfTwoNumbers is called the Higher Order function and the function which is passed as a parameter to it is known as callback function.

Usage of callback :

Basically a callback functions is used to handle Asynchronous operation, where this asynchronous operation is an operation which generally takes time to finish or complete the task like fetching data from server, reading a file, writing a file, making API requests, and executing timers.

example:

setTimeout(()=>{
    console.log("Hello");
},2000);

From the above example we can see that how callback function is used to handle the asynchronous operation.The setTimeout is a function which is provided by the Web API’s to JavaScript.This function is used to execute the task only after certain delay.It basically has two parameters , first is a call back function and the second is the delay in milli seconds.So when the delay is completed the callback function is invoked and will execute the task.

What is Callback Hell ? :

As the complexity of asynchronous operations increases, callbacks can lead to a phenomenon known as callback hell. Callback hell occurs when multiple nested callbacks are needed to handle successive asynchronous operations, making code difficult to read and maintain.

example:

setTimeout(()=>{
    console.log("Task 1");
    setTimeout(()=>{
        console.log("Task 2");
        setTimeout(()=>{
            console.log("Task 3");
        },2000)
    },2000)
},2000)

This callback hell is also known as Pyramid of Doom.This pyramid-like structure is hard to follow, debug, and refactor, and it can lead to errors.

Inversion of Control in Callbacks :

Callbacks introduce an issue known as inversion of control. When we pass a callback function, we essentially hand over control of the function to the external API or function. This can lead to unexpected behavior or potential errors, as the callback may not be called at all, or may be called multiple times. This lack of control over the execution flow is a major reason why callbacks can be problematic.

Why we need a better solution than Callback ? :

While callbacks work, they can lead to complex, hard-to read, and unmaintainable code ( callback hell ). Additionally, error handling becomes hard to manage and managing sequential or parallel asynchronous tasks becomes much more harder.

To solve these problems, modern JavaScript offers promises and async/await, which improves readability and provide a more robust way to handles asynchronous code.

Promises :

Promise Definition :

A promise is generally an object which eventually represents the completion or failure of an asynchronous operation.It is basically an container which is used to store future values.

There are basically three states in Promises

  1. Pending : Initially the promise is in pending state whenever an asynchronous operation starts.

  2. Fulfilled : When the asynchronous operation is successfully completed then the promise moves to the fulfilled state.

  3. Rejected : When the asynchronous operation is not successful or it is a failure then the promise moves to the rejected state.

The important thing about promises is that they are immutable, that means we cannot change or edit the properties present in the promise object.

Creating a Promise:

A promise can be created by using a new keyword along with the Promise constructor.This Promise constructor takes a function as a parameter, this function has two arguments resolve and reject.This resolve and reject are the functions which are provided by the JavaScript itself to build the promises.

example:

const newPromise = new Promise((resolve,reject)=>{
    setTimeout(()=>{
          resolve("Promise resolved");
    },2000);
);

From the above example we can see that a promise has been created and it will be resolved after 2 seconds with a message “Promise resolved” .Promise either has two result.

  1. success (or) fulfilled.

  2. failure (or) rejected.

Handling Async Operations with Callbacks and Promises:

Instead of chaining multiple callbacks, promises offer a then() method for handling success and catch() for handling errors, making the code more readable.

handling using callbacks :

function usingCallback(callback){
    setTimeout(()=>{
        let a = 10;
        let b = 20;
        let sum = a + b;
        callback(a,b,sum);
    },2000);
}

usingCallback((val1,val2,sum)=>{
    console.log(`The sum of ${val1} and ${val2} is ${sum}`);
});

The above represents handling the asynchronous operation using callbacks. This is an simpler example to understand it easily but handling using callback may become much more complex if we have multiple callbacks nested inside each other.This becomes very hard to manage error handling and the code will be unreadable because of its complexity and it becomes very tough to manage the code also.This is the main reason of using promises over callbacks to handle asynchronous operation.

handling using Promises:

function usingPromises(){
   return new Promise((resolve,reject)=>{
         setTimeout(()=>{
            let a = 10;
            let b = 20;
            let sum = a + b;
            resolve(sum);
         },2000);
    });
}

usinPromises().then((res)=>{
    console.log(`The sum of two numbers is ${res}`);
});

From the above code we can see that by using promises, the code is much more readable and manageable than by using callbacks.Here we have handled the promise by attaching a callback function but not by passing a function.This is the main advantage of it and callback hell can be reduced by using promise chaining which is much more readable and manageable.

Why promises are useful? :

When we work with asynchronous tasks ( like fetching data from a server ), we don’t know when the operation will complete, Instead of blocking the code until the operation finishes, a promise allows you to write code that can wait for the result and then continue processing, without freezing the rest of our program.

Promise chaining :

One of the biggest advantages of promises is chaining, where multiple asynchronous operations can be handled sequentially without falling into callback hell.

example:

getUserData()
    .then((user) => getPosts(user.id))
    .then((posts) => getComments(posts[0].id))
    .then((comments) => console.log(comments))
    .catch((error) => console.error(error));

In the above code each then() returns a new promise, allowing the next operation to be chained to it.

Promise APIs :

What are Promise APIs ? :

JavaScript provides several static methods under the Promise object that allow us to work with multiple promises concurrently. These include Promise.all(), Promise.allSetteled(), Promise.race(), and Promise.any().

  1. Promise.all() :

    Promise.all() is used to run multiple promises in parallel and waits for all of them to be resolved or rejected.

    It takes an array of promises as an input and returns an array of responses of each promise only when each promise gets resolved.

    But if any one of the promise is rejected then it will directly throw an error without waiting for the promises which are yet to be fulfilled

    example:

     const p1 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p1 resolved");
         },2000)
     });
     const p2 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p2 resolved");
         },1000)
     });
     const p3 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p3 resolved");
         },3000)
     });
    
     Promise.all([p1,p2,p3])
             .then((res)=> console.log(res))
             .catch((err)=> console.log(err));
    

    From the above example we can see that there are there are three promises created where p1 resolves in 2 seconds, p2 resolves in 1 second and p3 resolves in 3 seconds, so the Promise.all() will wait for 3 seconds as all the promises will get resolved and return an array of all the three promise responses.

  2. Promise.allSetteled() :

    Promise.allSetteled() waits for all promises to either resolve or reject, and returns an array of objects indicating the outcome of each promise.

    It overcomes (or) resolves the consequences of Promise.all().

    example :

     const p1 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p1 resolved");
         },2000)
     });
     const p2 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             reject("Promise p2 failed");
         },1000)
     });
     const p3 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p3 resolved");
         },3000)
     });
    
     Promise.allSettled([p1,p2,p3])
             .then((res)=> console.log(res))
             .catch((err)=> console.log(err));
    

    From the above example we can see that one of the three promises is rejected, even if any promise is rejected Promise.allSetteled() will return the array of all the response no matter if any of the promise is rejected unlink Promise.all().

  3. Promise.race() :

    Promise.race() returns the result of the first promise that resolves or rejects, whether successful or not.

    example :

     const p1 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p1 resolved");
         },2000)
     });
     const p2 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             reject("Promise p2 failed");
         },1000)
     });
     const p3 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p3 resolved");
         },3000)
     });
    
     Promise.race([p1,p2,p3])
             .then((res)=> console.log(res))
             .catch((err)=> console.log(err));
    

    From the above example we can see that promise p2 is settled ( resolved or rejected) first, so the Promise.race() will return the response of the promise.As the promise p2 is rejected, therefore the result will be an error with a message “Promise p2 failed”.

  4. Promise.any() :

    Promise.any() resolves as soon as any one of the promises is fulfilled. If all promises are rejected, it returns an AggregateError.

    example :

     const p1 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p1 resolved");
         },2000)
     });
     const p2 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             reject("Promise p2 failed");
         },1000)
     });
     const p3 = new Promise((resolve,reject)=>{
         setTimeout(()=>{
             resolve("Promise p3 resolved");
         },3000)
     });
    
     Promise.any([p1,p2,p3])
             .then((res)=> console.log(res))
             .catch((err)=> console.log(err));
    

    From the above example we can see that promise p2 is settled ( resolved or rejected) first, but it is rejected.Promise.any() only looks for the promise which is

    resolved first, so in our case it is the promise p1 which is resolved first after 2 seconds. Therefore the result will be the response of p1 with a message stating “Promise p1 resolved”.

What are Async/Await ? :

While promises make handling asynchronous code easier, the syntax can still be verbose and complex in some cases. To further simplify promise handling, JavaScript introduced async/await in ES8.

  • Async functions: These are functions that return a promise, and the await keyword can be used inside them to pause the execution until the promise resolves.

Usage of Async/Await :

async function fetchData() {
    try {
        let user = await getUserData();
        let posts = await getPosts(user.id);
        let comments = await getComments(posts[0].id);
        console.log(comments);
    } catch (error) {
        console.error(error);
    }
}

This syntax is much more readable and linear compared to promise chaining or nested callbacks.

Benefits of Async/Await :

  1. Readability: Async/await makes asynchronous code look more like synchronous code.

  2. Error Handling: Errors are caught using the try/catch block, simplifying error management.

0
Subscribe to my newsletter

Read articles from Abhishek Reddy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abhishek Reddy
Abhishek Reddy