Asynchronous Programming Patterns in JavaScript

Js Intermediate

Introduction

As we know in synchronous programming, tasks are executed one after the other sequentially, asynchronous programming allows a program to continue executing other tasks while waiting for asynchronous operations to complete. It is really important in conditions where resource utilization is critical, for example: network requests, file handling, user interfaces, etc. It allows applications to remain responsive to user input while performing time-consuming operations in the background, ultimately enhancing the user experience.

Async Programming Patterns

  1. Callback Functions

    They are functions that are passed as arguments to other functions and are executed at a later time, often after an asynchronous operation has been completed.

    A common issue while using callbacks extensively is callback hell, also known as the pyramid of doom, where deeply nested callbacks can make code hard to read and maintain and this can be avoided by using Promises.

     function makeRequest(callback) {
       // Simulating an asynchronous operation (e.g., making a HTTP request)
       setTimeout(function () {
         const data = { status: "Success" };
         callback(data); // Execute the callback function with the data
       }, 1000); // Simulate a 1-second delay
     }
    
     // Usage of the callback
     makeRequest(function (response) {
       console.log(response.status); // This code executes when the data is ready
     });
    
  2. Promises

    Promises are a way to handle asynchronous operations more efficient and effective way as compared to using traditional callback functions.

    Promises have 3 states while execution :

    1. Pending: it is the initial state when the execution has started and it is yet to be completed.

    2. Resolved: it is the state when execution is successfully completed and we have the result/data available.

    3. Rejected: it is the state where execution is completed with some error/exception with an error status message available.

      Creating a promise

       const fetchData = new Promise((resolve, reject) => {
         // some asyn operation here that gives 'data'
         if (data) {
           resolve(data);
         } else {
           reject(new Error("Some error message"));
         }
       });
      

      Consuming a promise

       fetchData
           .then((result) => {
                console.log("Success:", result);
           })
           .catch((err) => {
                console.log(err);
           })
      
  3. Async/await:

    It is a feature built on top of Promises that was released in ES2017, making the asynchronous code more readable and maintainable.

    An async function is a function that is declared with the async keyword before the function keyword.

     async function fetchData() {
       // Asynchronous operations here
     }
    

    Using await, in front of a Promise, it pauses the execution of the async function until the Promise is either resolved or rejected.

     async function fetchData() {
       const result = await someAsyncFunction();
       // you can access result now as if it was a synchronous value
     }
    

    Note: For error handling you can use try...catch

     async function fetchData() {
       try {
         const result = await someAsyncFunction();
         // Handle success here
       } catch (error) {
         // Handle error here
       }
     }
    
  4. Generators and Yield

    Generators are a unique JavaScript feature that allows you to pause and resume the execution of a function. The yield keyword is used within generators to yield control back to the caller. This function can have one or more yield expressions.

    It is declared using an asterisk (*) right after the function keyword.

     function* newGenerator() {
       // Generator function code here
     }
    

    Generators can be used to perform asynchronous operations sequentially by yielding promises and then using the await keyword, you can make asynchronous code appear sequential.

     // A function that simulates an asynchronous operation with a delay
     function asyncOperation(value) {
       return new Promise((resolve, reject) => {
         setTimeout(() => {
           console.log(`Async operation completed with value: ${value}`);
           resolve(value);
         }, 1000); // Simulate a 1-second delay
       });
     }
    
     // A generator function that performs sequential asynchronous operations
     function* asyncSequentialOperations() {
       try {
         const result1 = yield asyncOperation(1);
         const result2 = yield asyncOperation(2);
         const result3 = yield asyncOperation(3);
    
         console.log('All operations completed:', result1, result2, result3);
       } catch (error) {
         console.error('An error occurred:', error);
       }
     }
    
     // Helper function to run the generator
     function runAsyncSequentialOperations(generator) {
       const iterator = generator();
    
       function handleResult(result) {
         const { value, done } = iterator.next(result);
    
         if (!done) {
           value.then(handleResult).catch((error) => iterator.throw(error));
         }
       }
    
       handleResult();
     }
    
     // Run the sequential asynchronous operations
     runAsyncSequentialOperations(asyncSequentialOperations);
    

    Note: runAsyncSequentialOperations is a helper function that initializes the generator and handles the sequencing of asynchronous operations by calling iterator.next(result).

    Generators can also be used to manage complex control flow, such as loops or conditional logic, within asynchronous code. You can pause the generator until certain conditions are met and then resume it when the conditions are satisfied.

     function* generatorLoop() {
       let count = 0;
       while (count < 5) {
         yield asyncOperation(count);
         count++;
       }
     }
    

    While generators and yield can simplify the handling of asynchronous code, it's important to note that async/await is now the preferred and more widely adopted approach for managing asynchronous operations in JavaScript.