Asynchronous JavaScript

Asynchronous programming is an essential concept in JavaScript that allows your code to run in the background without blocking the execution of other code. Developers can create more efficient and responsive applications by using features like callbacks and promises.

How JavaScript executes the code?

  • JavaScript is a single-threaded programming language, meaning it has only one call stack and one thread of execution. This means that it can only do one thing at a time, executing code in a sequential manner i.e. line-by-line manner.

  • JavaScript uses a Call Stack to track the functions in a program. The call stack works on the Last In, First Out (LIFO) principle. This means that the most recently called function will be the first to be completed. Whenever a function is called, a new frame is added to the top of the stack. As function is executed, they're popped off the stack, allowing the next function in line to be executed. Let's illustrate it with an example:

            function greet(name) {
              console.log(`Hello, ${name}!`);
            }
    
            function welcome() {
              console.log("Welcome to our website!");
            }
    
            function main() {
              greet("Alice");
              welcome();
            }
            main();
            /* Output:
            Hello, Alice!
            Welcome to our website!
            */
    

What is difference between Sync & Async?

  • In synchronous operations, tasks are executed one after another, in sequence. Each task must complete before the next one can begin. This blocking behavior means that if one task takes a long time to execute, it will delay the execution of subsequent tasks, potentially causing the application to become unresponsive. For example:

            console.log("Task 1");
            console.log("Task 2");
            console.log("Task 3");
            /* Output:
            Task 1
            Task 2
            Task 3
            */
    
  • In asynchronous operations, tasks are executed independently of one another. This means that while one task is being processed, the program can continue executing other tasks without waiting for the asynchronous operation to complete. Asynchronous operations are non-blocking, allowing for more responsive and efficient code execution. For example:

            console.log("Task 1");
            setTimeout(() => {
              console.log("Task 2");
            }, 2000);
            console.log("Task 3");
            /* Output:
            Task 1
            Task 3
            Task 2 (It prints after 2 seconds due to setTimeout function)
            */
    

How can we make sync code into async?

To convert synchronous code into asynchronous code in JavaScript, we can use various techniques such as callbacks and promises syntax. Here's how we can achieve asynchronous behavior using each of these methods:

  1. Callbacks:

    Callbacks are functions passed as arguments to other functions, to be executed once a task is complete. They are a simple and widely used technique for handling asynchronous operations in JavaScript. For example:

     function fetchData(callback) {
       setTimeout(() => {
         const data = "Async data";
         callback(data);
       }, 2000);
     }
     function processData(data) {
       console.log("Processing data:", data);
     }
     fetchData(processData); // Call fetchData with a callback function
     // Output: Processing data: Async data
    
  2. Promises:

    Promises provide a cleaner and more structured way to handle asynchronous operations. A promise represents the eventual completion or failure of an asynchronous operation and allows us to chain multiple asynchronous tasks together. For example:

     function fetchData() {
       return new Promise((resolve, reject) => {
         setTimeout(() => {
           const data = "Async data";
           resolve(data);
         }, 2000);
       });
     }
    
     fetchData()
       .then((data) => {
         console.log("Processing data:", data);
       })
       .catch((error) => {
         console.error("Error:", error);
       });
     // Output: Processing data: Async data
    

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

  • In JavaScript, a callback is a function that is passed as an argument to another function and is intended to be executed after a particular task or event occurs.

  • Callbacks are commonly used in asynchronous programming to handle the completion of tasks such as network requests, file I/O, or user interactions.

  • Drawbacks of using callbacks:

    1. Callback Hell (Pyramid of Doom): When multiple asynchronous operations are nested within each other, the code can become deeply nested, leading to a phenomenon known as "callback hell." This makes the code difficult to understand and maintain. Also, error handling is difficult in this case.

      For example:

       fetchData(function(data) {
         processFirstData(data, function(result) {
           processSecondData(result, function(finalResult) {
             // More nested callbacks...
           });
         });
       });
      
    2. Inversion of Control: With callbacks, the flow of control is distributed across multiple functions, making it harder to reason about the program's execution flow and leading to potential bugs and errors.

How promises solves the problem of inversion of control?

  • Promises in JavaScript provide a solution to the problem of inversion of control that is commonly associated with callback-based asynchronous code.

  • Inversion of control refers to the scenario where the flow of control is dictated by an external system, rather than by the code itself.

  • Promises solves the problem ofinversion of control by:

    1. Promise Chaining:

      Promises introduce a chaining mechanism that enables the sequential execution of asynchronous tasks in a more readable and maintainable manner. This eliminates the need for nested callbacks, reducing the likelihood of callback hell and improving code readability. For example:

       fetchData()
         .then(processData1)
         .then(processData2)
         .then(finalResult => {
           console.log("Final Result:", finalResult);
         })
         .catch(error => {
           console.error("Error:", error);
         });
      
    1. Error Handling:

      Promises provide a standardized mechanism for error handling through the .catch() method. This allows developers to handle errors in a centralized manner, making code more robust and easier to maintain. For example:

         fetchData()
           .then(processData)
           .catch(error => {
             console.error("Error:", error);
           });
      

What is event loop?

  • In JavaScript, the event loop is a fundamental mechanism that enables the asynchronous execution of code.

  • It's an essential part of the JavaScript runtime environment, allowing the language to handle non-blocking operations efficiently.

  • The event loop continuously checks the call stack and the event queue.

  • If the call stack is empty, the event loop takes the first message from the event queue and pushes it onto the call stack, where it is executed.

  • This process repeats indefinitely, ensuring that asynchronous tasks are executed in the order they were received and that the call stack remains unblocked.

      console.log("Start");
    
      setTimeout(() => {
        console.log("Timeout");
      }, 2000);
    
      Promise.resolve().then(() => {
        console.log("Promise");
      });
    
      console.log("End");
      /* Output:
      Start
      End
      Promise
      Timeout
      */
    

What are different functions in promises?

  1. `Promise.all()` :

    This method takes an array of promises as input and returns a single Promise that resolves when all of the input promises have resolved, or rejects if any of the input promises reject. For example:

     const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, "Promise 1"));
     const promise2 = new Promise((resolve) => setTimeout(resolve, 1500, "Promise 2"));
     const promise3 = new Promise((resolve) => setTimeout(resolve, 2000, "Promise 3"));
    
     Promise.all([promise1, promise2, promise3])
       .then((values) => {
         console.log(values);
       });
     // Output: ["Promise 1", "Promise 2", "Promise 3"]
    
  2. `Promise.race()` :

    This method takes an array of promises as input and returns a single Promise that resolves or rejects as soon as one of the input promises resolves or rejects. For example:

     const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, "Promise 1"));
     const promise2 = new Promise((resolve) => setTimeout(resolve, 2000, "Promise 2"));
     const promise3 = new Promise((resolve, reject) => setTimeout(reject, 500, "Promise 3"));
    
     Promise.race([promise1, promise2, promise3])
       .then((value) => {
         console.log(value); // Output: Promise 1
       })
       .catch((error) => {
         console.error(error); // Output: Promise 3
       });
    
  3. `Promise.resolve()` :

    This method returns a Promise object that is resolved with the given value. If the value is a promise, it is returned unchanged. For example:

     const resolvedPromise = Promise.resolve("Resolved value");
    
     resolvedPromise.then((value) => {
       console.log(value); // Output: Resolved value
     });
    
  4. `Promise.reject()` :

    This method returns a Promise object that is rejected with the given reason (error). It is typically used to handle errors in asynchronous operations. For example:

     const rejectedPromise = Promise.reject("Error message");
    
     rejectedPromise.catch((error) => {
       console.error(error); // Output: Error message
     });
    
  5. `Promise.any()`:

    This method takes an array of promises and returns a single Promise that resolves as soon as any of the input promises fulfill (resolve), or rejects if all of the input promises reject. For example:

     const promise1 = new Promise((resolve, reject) => setTimeout(reject, 1000, "Promise 1 failed"));
     const promise2 = new Promise((resolve) => setTimeout(resolve, 2000, "Promise 2 resolved"));
     const promise3 = new Promise((resolve) => setTimeout(resolve, 1500, "Promise 3 resolved"));
    
     Promise.any([promise1, promise2, promise3])
       .then((value) => {
         console.log("First resolved promise:", value);
       })
       .catch((error) => {
         console.error("All promises rejected:", error); // This won't be executed in this example
       });
     // Output: First resolved promise: Promise 3 resolved
    
  6. `Promise.anySettled()`:

    It is a method that takes an array of promises as input and returns a single promise. This promise resolves after all the input promises have settled (either fulfilled or rejected), with an array of settlement results for each promise. For example:

     const promise1 = Promise.resolve("Resolved value 1");
     const promise2 = Promise.reject("Rejected value 2");
     const promise3 = new Promise((resolve) => setTimeout(resolve, 2000, "Resolved value 3"));
    
     Promise.anySettled([promise1, promise2, promise3])
       .then((results) => {
         console.log(results);
     )};
     /*    Output:
           [
             { status: 'fulfilled', value: 'Resolved value 1' },
             { status: 'rejected', reason: 'Rejected value 2' },
             { status: 'fulfilled', value: 'Resolved value 3' }
           ]
     */
    

Conclusion:

  • Asynchronous programming in JavaScript helps your code run smoothly in the background without slowing down other tasks.

  • By using features like callbacks and promises, you can make your applications more efficient and responsive.

  • JavaScript executes code sequentially, one step at a time.

  • Synchronous operations happen one after another, while asynchronous operations allow tasks to run independently, making your code faster and more flexible.

  • Callbacks are functions that get called after a specific task finishes. They're handy for handling asynchronous tasks, but they can get messy when dealing with complex operations.

  • Promises come to the rescue by providing a cleaner and more organized way to manage asynchronous code. They make error handling easier and allow you to chain multiple tasks together seamlessly.

  • You can use different methods of promises to perform different tasks or fetch relevant data according to the needs.

Happy coding!