Exploring the Concepts of Asynchronous JavaScript

How does JavaScript execute the code?

JavaScript is a synchronous, single-threaded programming language. It executes a single line of code at a time and it keeps the further parts on hold until the current line of code doesn't get executed. It executes the code in sequential order from top to bottom. We can make JavaScript do asynchronous operations using callbacks, promises, and async-await. In asynchronous operation the execution does not hold on a particular line of code instead it executes the further codes.

When we run any JavaScript code, a global execution context is created and placed inside the call stack. In JavaScript, every execution takes place in the call stack. The global execution context consists of two parts: memory and code.

When we run our code, the execution happens in two phases one is the memory allocation phase and the other one is the code execution phase. In the memory allocation phase, the variables and the functions are initialized inside the memory part of the global execution context. The variables get initialized with 'undefined' and the functions get initialized with the actual code of that function. After the memory allocation phase, the code execution phase starts and all the variables get initialized with the actual values given in the code. When it encounters a function call then a new execution context for that function gets created and it gets placed inside the call stack. Now all the variables and functions inside the function get initialized the same way inside the execution context of that function. When the execution of that function is completed the execution context of that function gets popped out of the call stack and it starts executing the further codes of the global execution context. And finally when all codes of the global execution context get executed then the global execution context also gets popped out of the call stack.

This was the execution process of synchronous code in JavaScript. The execution of asynchronous code is also similar, but there are new elements called callback and microtask queues. The Microtask queue has more priority than the callback queue. Callback functions get stored in the callback queue and callback functions that come from promises get stored in the microtask queue. The event loop keeps track of the call stack and these queues and places the callback functions from these queues to the call stack to execute them.

What is the difference between Sync & Async?

These are the differences between synchronous and Asynchronous JavaScript Operations:

  • Sync is single-threaded but async is multi-threaded: A single line of code gets executed at a time in sync but in async multiple lines of code can get executed in parallel.

  • Sync is blocking but async is non-blocking: Sync blocks the further lines of code but async doesn't.

  • Sync is sequential but async is non-sequential.

  • Sync is easy to understand and debug but async is not.

How can we make sync code into async?

We can make sync code into async code using callback functions, promises, and async-await. These allow the codes to run asynchronously and do not block further code execution.
Using callbacks:

api.call();

console.log("Hello");

In the above code, if it takes some time to fetch data from the API, the further execution of the code gets blocked, and "Hello" does not get logged.

We can make this sync code into async code using callback functions.

setTimeout(() => {
    api.call();
}, 5000);

console.log("Hello");

In this code, the execution doesn't get held instead it executes the further line of code.

Using Promises:

api.call()   
    .then(() => {
        // codes to be executed only after the api call
    });

console.log("Hello");

The above code also does not block the execution of the further lines of code.

We can do this using async-await also.

What are callbacks & what are the drawbacks of using callbacks?

A callback is a javascript function that is passed as an argument into another function. We use callbacks if we want to call a function only after completion of one task.

Javascript is a single-threaded, synchronous language that executes the code sequentially from top to bottom. Callback enables us to do things in JavaScript asynchronously.

Example:

console.log("Hello");

If we run the above code the "Hello" will be printed instantly after hitting the run button. But, if we want to print the "Hello" after a few seconds(or after the completion of something in the code), then we can do this by using the callback function.

setTimeout(function(){
    console.log("Hello");
}, 5000);

Here in the code we have wrapped the "console.log("Hello")" inside a function and passed it to the setTimeout() function as a callback function. Now if we run this code then the "Hello" will be printed on the console after 5sec.

Disadvantages of Callback Function:

The callback function is very useful and powerful despite that there are some disadvantages of using a callback function that can be very dangerous at times. Here are some disadvantages of using the callback function:

  1. Callback Hell: If we use pass a callback function inside a function, pass that function inside another function, and so on using nested function, then the code grows horizontally instead of vertically. It becomes very hard to handle, manage, and read this kind of code. for example:
taksOne(param1, () => {
    console.log("Callback function inside taskOne.");

    taskTwo(param2, () => {
        console.log("Callback function inside taskTwo.");

        taskThree(param3, () => {
            console.log("Callback function inside taskThree.");

            taksFour(param4, () => {
                console.log("Callback function inside taskFour.");
                .
                .
                .

            })
        })
    })
})

This is a simple example where we can still be able to read, and manage the code, but in real code, it becomes very difficult to handle and manage this thing as there are hundreds of lines of code inside a single function.

  1. Inversion of Control: When we pass a callback function to a function then we lose control over the callback function and we give control of the callback function to that function. In this kind of case if there occurs an error inside the main function then the callback function might not be called or might not be called as we were expecting it to be. This can be very dangerous in real-life code if there occurs some error in the main function.
api.createOrder(cart, () => {

    api.initiatePayment();
})

In the above code if there occurs an error in the API call to create an order then there can be a case that the initiate payment API call does differently than expected or it did not get called.

How do promises solve the problem of inversion of control?

In the case of the callback function, we pass our code(as a callback function) to the function after which we want to execute our code and we lose control over our code. In this case, there occurs the problem of inversion of control. In this kind of situation, the control of our code is in the hand of the function and the function executes our code. There might be some cases when there is some error in that function and our code doesn't get executed(called the callback function) or gets executed differently than we were expecting it to be. This situation is called "Inversion of Control".

This can be solved by using promises in our code. In case of promises we do not pass our code to the function but we attach our code to the function and the function does not have any control over our code. We will be the ones to call our code to execute.
Example:

api.createOrder(cart, () => {

    api.initiatePayment();
})

In the above code, we have passed the initiatePayment API call to the createOrder API call as a callback function and given the control of the initiatePayment API call to the createOrder API call.

api.createOrder()   
    .then(() => {
        api.initiatePayment();
    });

In the above code, we have used promises and attached the initiatePayment API call to the createOrder API call and the createOrder API call does not have any control over the initiatePayment API call. The initiatePayment API will be called only after the complete execution of the createOrder API call.

What is an event loop?

Even loop is a utility of JavaScript that handles the asynchronous operations of the JavaScript. It coordinates between the callback queue and the call stack and transfers the execution context from the callback queue to the call stack. The event loop continuously checks the call stack and the callback queue. If the call stack is empty, it takes the first function from the callback queue and pushes it onto the call stack for execution.

What are the different functions of promises?

There are four major functions in Promise API.

  1. Promise.all(): It waits until it encounters the first rejection (returns error corresponding to that promise) or all promises get resolved (returns array of responses of all promises).

     Promise.all([p1, p2, p3])
         .then((result) => {
             console.log(result);
         })
         .catch((err) => {
             console.error(err);
         })
    
  2. Promise.allSettled(): It waits until all promises get settled and returns an array of objects (Each object includes two properties status and value(in case of success) or reason(in case of rejection)).

     Promise.allSettled([p1, p2, p3])
         .then((result) => {
             console.log(result);
         })
         .catch((err) => {
             console.error(err);
         })
    
  3. Promise.race(): It waits until the first settlement(resolve or rejection) and returns the response(in case of resolve) or error(in case of rejection)

     Promise.race([p1, p2, p3])
         .then((result) => {
             console.log(result);
         })
         .catch((err) => {
             console.error(err);
         })
    
  4. Promise.any(): It waits until the first resolve happens and returns the response of the promise. If all promises get rejected then it returns AggregateError(an object with one property 'Errors' whose value is an array of error responses of all the promises).

     Promise.any([p1, p2, p3])
         .then((result) => {
             console.log(result);
         })
         .catch((err) => {
             console.error(err);
         })