17. Asynchronous Javascript - Part 2 : CallBacks , Callback hell , Promises , async/await

  1. Callbacks

    A callback function is a function passed as an argument to another function and executed later.

function fetchData(callback) {
    setTimeout(() => {
        console.log("Data fetched");
        callback();
    }, 2000);
}

function processData() {
    console.log("Processing data...");
}

fetchData(processData);

Read More about callbacks in previous article:

Click Here

Issues with Callbacks

  • Hard to manage in complex applications.

  • Leads to callback hell when multiple nested callbacks exist.

  1. Callback hell

    Callback Hell (also known as Pyramid of Doom) occurs when multiple asynchronous operations are nested inside each other, making the code unreadable and difficult to maintain.

     function step1(callback) {
         setTimeout(() => {
             console.log("Step 1 completed");
             callback();
         }, 1000);
     }
    
     function step2(callback) {
         setTimeout(() => {
             console.log("Step 2 completed");
             callback();
         }, 1000);
     }
    
     function step3(callback) {
         setTimeout(() => {
             console.log("Step 3 completed");
             callback();
         }, 1000);
     }
    
     step1(() => {  
         step2(() => {
             step3(() => {
                 console.log("All steps completed");
             });
         });
     });
    
     /*
     Step 1 completed
     Step 2 completed
     Step 3 completed
     All steps completed
    

    Problems with Callback Hell

    • Code becomes deeply nested.

    • Hard to debug and maintain.

    • Difficult to handle errors.

To solve these issues, Promises were introduced.

  1. Promises

    In JS Promises is a good way to handle asynchronous operations . It is used to find out if the asynchronous operation is successfully completed or not.

    It has three states:

    1. Pending: Initial state.

    2. Resolved (Fulfilled): Operation completed successfully.

    3. Rejected: Operation failed.

       //Step 1: Creating a promise
       const ticket = new Promise((resolve, reject)=>{
           const isBoarded = false;  // our promise is reject
           if(isBoarded){
               resolve("Your is on the time");
           }
           else{
               reject("Your flight has been cancelled");
           }
       })
      
       // Step 2:use a promise -> .then if resolve , .catch() if reject 
       ticket.then((data)=>{   // data is that is written inside reolve () or reject()
           console.log("WOW", data)
       }).catch((data)=>{
           console.log("Oh No",data)  // Output: Oh No Your flight has been cancelled
       })
      

      Promise solve the problem of callback hell : Chaining Promises

       function step1() {
           return new Promise((reolve,reject)=>{
               setTimeout(() => {
                   console.log("Step 1 completed");
                    reolve()
               }, 1000);
           })   
       }
      
       function step2() {
           return new Promise((reolve,reject)=>{
               setTimeout(() => {
                   console.log("Step 2 completed");
                   reolve()
               }, 1000);
           })   
       }
      
       function step3() {
           return new Promise((reolve,reject)=>{
               setTimeout(() => {
                   console.log("Step 3 completed");
                   reolve()
               }, 1000);
           })   
       }
      
       step1().then(() => step2())  // return promise
              .then(() => step3())
              .then(() => console.log("All steps completed")) 
              .catch((error) => console.error("Error:", error));
      
       /*
       Step 1 completed
       Step 2 completed
       Step 3 completed
       All steps completed
      
  1. Async/Await

    async/await is a cleaner way to handle promises. It makes asynchronous code look like synchronous code.

     // Step 1: Creating a promise
     const ticket = new Promise((resolve, reject) => {
         const isBoarded = false; // our promise is rejected
         if (isBoarded) {
             resolve("Your flight is on time");
         } else {
             reject("Your flight has been cancelled");
         }
     });
    
     // Step 2: Using async/await
     async function checkFlight() {
         try {
             const data = await ticket; // Waiting for the promise to resolve
             console.log("WOW", data);
         } catch (error) {
             console.log("Oh No", error); // Output: Oh No Your flight has been cancelled
         }
     }
    
     // Calling the function
     checkFlight();
    

    Error Handling in Async/Await:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let success = false;
            if (success) {
                resolve("Data fetched successfully");
            } else {
                reject("Error fetching data");
            }
        }, 2000);
    });
}

async function getData() {
    try {
        let data = await fetchData();
        console.log(data);
    } catch (error) {
        console.log(error);
    }
}
getData();

Advantages of Async/Await:

  • Improves readability.

  • Easier error handling with try/catch.

  • Avoids chaining problems in promises.

Interview Questions

  1. How do Promises solve callback hell?

    Promises allow chaining .then() instead of deeply nesting callbacks.

  1. Predict the output ?

     console.log("Start");
     setTimeout(() => console.log("Timeout"), 0);
     Promise.resolve().then(() => console.log("Promise")); 
     console.log("End");
    
     /*
     Start
     End
     Promise
     Timeout
    
    • Synchronous logs "Start" and "End".

    • Promise.then() executes before setTimeout() due to microtask queue priority.

  1. Predict the output ?

     async function foo() {
         console.log(1);
         await console.log(2);
         console.log(3);
     }
     console.log(4);
     foo();
     console.log(5);
    
     /*
     4
     1
     2
     5
     3
    

    Explanation:

    • await console.log(2) logs 2, but await makes console.log(3) execute later.

    • console.log(5) executes before console.log(3).