Async JavaScript

Photo by Andrew Neel on Unsplash

Async JavaScript

Although JavaScript is a Synchronous language (which means it does one task at a time), It handles concurrency using Asynchronous functions. Using this property JS can execute code in a non-blocking behavior and make JS apps highly scalable. It also enables JS to enhance user experience and improve the app's performance.

Async js picture

How does JS execute the code?

JavaScript is a single-threaded language, we might have heard this phrase a lot of times but what does it mean? For example, we have a code with a while loop with a million iterations and it will run when the page loads and we have a button on the screen. so till the time the loop will run the user screen will freeze because JS is busy running the loop so in that time we can't use the button or do anything. This results in a poor user experience.

Also, JS only has one call stack which is used to manage the JS code, to manage when a piece of code will run. when we run a code first of all a GEC(Global Execution Context) is created which is pushed to the call stack. and then JS engine will execute code line by line and keep adding the functions present in the code into the stack and when the function is done executing it will pop it off the stack and go to the next function until we get to the end of the program.

For running Async code JS engine will offload tasks to web APIs (also known as browser APIs) and keep running the rest of the code synchronously. Web APIs perform the task given to them and when the async function is done executing, the callback attached to it will be put on the callback queue(or microtask queue if it was a promise), and then when all code is done executing, Event loop (more on this later) check if the call stack is empty or not and if it is empty it starts putting callback from the queue to the call stack, which in turn executes the callback and all code will be done executing.

What is the difference between Sync & Async?

Sync means that our code will run line by line and every statement will execute the way it was written.

console.log("Start");
console.log("Middle");
console.log("End");
//prints
/*
Start
Middle
End
*/

Async code will be given to the Web APIs and will execute after all the sync code.

console.log("Start");

setTimeout(function() {
    console.log("Middle");
}, 1000);

console.log("End");
//prints 
/*
Start
End
Middle 
*/
//prints Middle after 1 seconds

In sync code compiler will wait for every line to finish executing and then move to the next line, But in the case of async compiler will not wait for setTimeout to finish and moves to the next line and execute the code inside setTimeout in the end.

How can we make sync code into async?

Sync code can be made async using callbacks, Promises and async await.

Using Callbacks:-

function getData(callback) {
    // Simulating asynchronous operation (e.g., fetching data from a server)
    setTimeout(function() {
        const data = "JS is awesome";
        callback(data);
    }, 1000);
}

console.log("Start");
getData(function(data) {
    console.log("Data:", data);
});
console.log("End");
//prints 
/*
Start
End
Data:JS is awesome
*/

Using Promises:-

function getData() {
    return new Promise(function(resolve, reject) {
        // Simulating asynchronous operation (e.g., fetching data from a server)
        setTimeout(function() {
            const data = "JS is awesome";
            resolve(data);
        }, 1000);
    });
}

console.log("Start");

getData()
    .then(function(data) {
        console.log("Data:", data);
    })
    .catch(function(error) {
        console.error("Error:", error);
    });

console.log("End");

Using async await:-

async function fetchData() {
    // Simulating asynchronous operation (e.g., fetching data from a server)
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            const data = "Some data";
            resolve(data);
        }, 1000);
    });
}

async function fetchDataAndLog() {
    console.log("Start");
    const data = await fetchData();
    console.log("Data:", data);
    console.log("End");
}

fetchDataAndLog();

What are callbacks & what are the drawbacks of using callbacks

Callbacks are the functions that are passed to another function as an argument and are executed after a certain task is completed. These callbacks are generally used to implement async code in JavaScript.

Here's an example of callbacks:-

function fetchData(callback) {
    // Simulating an asynchronous operation (e.g., fetching data from a server)
    setTimeout(function() {
        const data = "Some data";
        callback(data);
    }, 1000);
}

// Usage:
fetchData(function(data) {
    console.log("Data received:", data);
});

In this example, the fetchData function takes a callback function as an argument and invokes it with the fetched data after a simulated delay.

Drawbacks of using callbacks:

  1. Callback Hell: Nested callbacks can lead to "callback hell," where code becomes deeply nested and difficult to read and maintain. This occurs when multiple asynchronous operations depend on the results of each other, resulting in deeply nested callback functions.

  2. Error Handling: Error handling in callback-based code can become complex, especially when dealing with multiple asynchronous operations. Error-handling logic tends to be spread across multiple callback functions, making it harder to track and debug errors.

  3. Inversion Of Control: When we pass the callback to a higher-order function, we are essentially giving control to that function of our callback. This HOF might call our callback multiple times or not even call it at all. This situation is known as inversion of control in callbacks. And if our callback is very crucial to us then this is a big drawback.

How do promises solve the problem of inversion of control?

So in callbacks, when we are passing the callbacks to another function that will result in an inversion of control. For example, in an e-commerce app, we are writing a function for creating an order for which we were given a function, and let's say we are passing a callback for proceeding to payment which will be called after the order is created.

const cart = ["Shoes","Bag","Jeans","Shirt"];
function createOrder(cart,proceedToPayment);

here we have no control over our callback and it is the createOrder function's responsibility to call it, which is a drawback.

But in promises createOrder function will return a promise and we will get the data using .then on it and then we can ourself call our callback.

const promise = createOrder(cart);
promise.then((orderId)=>{
    proceedToPayment(orderId);
})

so in this way, the control of code is with us the whole time.

What is an event loop?

Event loop is a fundamental concept in JavaScript. It's responsible for managing asynchronous operations and ensuring that JavaScript remains single-threaded and non-blocking, which is crucial for handling concurrent tasks efficiently.

Here's a simplified view of how the event loop works:-

  • CallStack:- Call stack keeps track of function calls and their execution contexts. JavaScript code execution begins with the call stack. When a function is called it is added to the top of the call stack and when it returns it is popped off the stack.

  • Web APIs/Browser APIs:- when the JS engine encounters an async function it is sent to web APIs to resolve or execute. For example, when fetching data from a server, when the JS engine comes across a fetch call it got send to the web APIs to handle.

  • Callback queue:- When Asynchronous operations offloaded to web APIs complete, their associated callback functions are pushed onto the event queue.

  • Event loop:- The event loop continuously checks the call stack and the event queue. If the call stack is empty and there are pending tasks in the event queue, the event loop moves the first task from the event queue to the call stack, where it can be executed.

    This process is repeated if there is an async function in the callback.

What are the different functions/APIs in promises?

There are different functions in promises. These functions are powerful tools for managing asynchronous operations in JavaScript and provide different strategies for handling multiple promises.

  1. Promise.resolve(value):

     Promise.resolve(10)
         .then(function(value) {
             console.log("Resolved:", value);
         });
     //prints:- Resolved: 10
    

    This will create a Promise that is resolved with the specified value.

  2. Promise.reject(reason):

     Promise.reject(new Error("Not good"))
         .catch(function(reason) {
             console.error("Rejected:", reason);
         });
     //prints:- Rejected:Not good
    

    This will create a Promise that is rejected with the specified reason.

  3. Promise.all(iterable):

     const promise1 = Promise.resolve(1);
     const promise2 = Promise.resolve(2);
     const promise3 = Promise.resolve(3);
    
     Promise.all([promise1, promise2, promise3])
         .then(function(values) {
             console.log("All promises resolved with:", values);
         })
         .catch(function(error) {
             console.error("An error occurred:", error);
         });
     //Output:- All promises resolved with: [ 1, 2, 3 ]
    

    This will create a Promise that resolves when all promises in the iterable have resolved, or rejects with the reason of the first promise that rejects.

  4. Promise.allSettled(iterable):

     const promise1 = Promise.resolve(1);
     const promise2 = Promise.reject(new Error("Promise 2 failed"));
     const promise3 = new Promise(function(resolve) {
         setTimeout(resolve, 1000, 3);
     });
    
     Promise.allSettled([promise1, promise2, promise3])
         .then(function(results) {
             console.log("All promises settled with:", results);
         });
     /* Output:-
     All promises settled with: [
       { status: 'fulfilled', value: 1 },
       {
         status: 'rejected',
         reason: Error: 'Promise 2 failed'
       },
       { status: 'fulfilled', value: 3 }
     ]
     */
    

    This will create a Promise that resolves after all promises in the iterable have settled (either resolved or rejected), returning an array of objects representing the outcome of each promise.

  5. Promise.any(iterable):

     const promise1 = new Promise(function(resolve) {
         setTimeout(resolve, 1000, "Promise 1 resolved");
     });
     const promise2 = Promise.reject(new Error("Promise 2 rejected"));
     const promise3 = Promise.resolve("Promise 3 resolved");
    
     Promise.any([promise1, promise2, promise3])
         .then(function(value) {
             console.log("At least one promise resolved with:", value);
         })
         .catch(function(error) {
             console.error("All promises were rejected:", error);
         });
     /*Output:- 
     At least one promise resolved with: Promise 3 resolved
     */
    

    This will create a Promise that resolves when any of the promises in the iterable resolves or rejects if all promises are rejected with an aggregated error.

  6. Promise.race(iterable):

     const promise1 = new Promise(function(resolve) {
         setTimeout(resolve, 1000, "Promise 1 resolved");
     });
     const promise2 = new Promise(function(resolve) {
         setTimeout(resolve, 500, "Promise 2 resolved");
     });
    
     Promise.race([promise1, promise2])
         .then(function(value) {
             console.log("First promise to resolve:", value);
         });
     /*Output:-
     First promise to resolve: Promise 2 resolved
     */
    

    This will create a Promise that resolves or rejects as soon as one of the promises in the iterable resolves or rejects, with the value or reason from that promise.