For enquiries call:

Phone

+1-469-442-0620

Easter Sale-mobile

HomeBlogWeb DevelopmentAsync Await in Node.js – How to Master it?

Async Await in Node.js – How to Master it?

Published
23rd Jan, 2024
Views
view count loader
Read it in
10 Mins
In this article
    Async Await in Node.js – How to Master it?

    When I entered the Node.js domain, the non-blocking nature of asynchronous code execution perplexed me. Discovering promises and node js async await function proved instrumental in achieving synchronous behavior.  Async await is originally part of JavaScript ECMAScript 2017, as Nodejs too works on JavaScript same concept can be used there too. Before delving into async await in nodejs, let's distinguish between asynchronous and synchronous programming. Asynchronous programming allows independent task execution, beneficial in Node.js for efficient handling of I/O operations. Conversely, synchronous programming executes tasks sequentially, potentially causing performance bottlenecks. async and await in nodejs simplifies synchronous coding in Node.js, replacing nested callbacks. In this article, we'll explore the intricacies of async await in Node js and uncover effective mastery patterns for this paradigm.

    If you want to be a professional node js developer, I recommend you go for this Node.js full course by KnowledgeHut.

    What are Async Functions in NodeJS?

    In Node.js, async functions, marked with the async keyword, simplify asynchronous code by offering a cleaner and more readable syntax. These functions allow the use of the await keyword, enabling the sequential and synchronous-like execution of asynchronous operations. When encountering an await expression, the async function pauses until the awaited promise resolves, eliminating the need for callbacks or intricate Promise chaining. Async functions significantly improve code readability and maintainability, particularly in scenarios involving I/O operations like reading files, making API requests, or querying databases.

    Async Functions Patterns in JavaScript

    Async Functions Patterns in JavaScript

    Patterns with async functions in JavaScript refer to common strategies and structures employed when working with asynchronous operations. These patterns aim to enhance code readability, maintainability, and the overall developer experience. Here are some prevalent patterns that is used to manage asynchronous execution:

    1. Retry with Exponential Backoff:

    In scenarios where network operations face intermittent failures, the Retry with Exponential Backoff pattern becomes invaluable. By implementing this pattern with async functions, such as in the example of fetching data from an API, the code intelligently retries the operation with progressively increasing delays. This not only improves the chances of successfully completing the operation but also prevents overwhelming the server with rapid retry attempts.

    Achieving this without using async and await in Node js becomes quite clumsy as seen below:

    function fetchDataWithRetry(retriesLeft = 3, delay = 1000) {
     return fetchData()
     .then((data) => {
     // Check if the data meets the desired condition
     if (data !== 'Expected Data') {
     if (retriesLeft > 0) {
     // Retry with exponential backoff
     console.log(`Retrying in ${delay} milliseconds...`);
     return new Promise((resolve) => {
     setTimeout(() => {
     resolve(fetchDataWithRetry(retriesLeft - 1, delay * 2));
     }, delay);
     });
     } else {
     // No more retries left, throw an error
     throw new Error('Max retries reached. Unable to fetch expected data.');
     }
     }
    // Data meets the condition, resolve the promise
     return data;
     });
    }
    // Simulating an asynchronous operation
    function fetchData() {
     return new Promise((resolve) => {
     // Simulate fetching data asynchronously
     setTimeout(() => {
     resolve('Expected Data');
     }, 2000);
     });
    }
    
    // Example usage
    fetchDataWithRetry()
     .then((result) => {
     console.log('Fetched Data:', result);
     })
     .catch((error) => {
     console.error('Error:', error.message);
     });

    Example:

    `fetchDataWithRetry` is a function that fetches data and retries with exponential backoff if the fetched data does not meet the expected condition.

    The `fetchData` function simulates an asynchronous operation.

    The `.then` chain handles retries, exponentially increasing the delay between retries.

    The example usage demonstrates fetching data with retry and handling success or error accordingly.

    The same process code becomes so easy to understand when used with async await node js concepts as shown in below node js async await function example:

    async function fetchDataWithRetry(url, maxRetries = 3) {
      let retries = 0;
     
     while (retries < maxRetries) {
      try {
      const response = await fetch(url);
      return response.json();
      } catch (error) {
      // Implement exponential backoff logic here
      await new Promise(resolve => setTimeout(resolve, 2 ** retries * 1000));
      retries++;
      }
      }
     throw new Error('Max retries reached');
      }

    2. Intermediate Values:

    The Intermediate Values pattern is crucial when dealing with asynchronous operations where data from one step is needed before initiating the next. For instance, consider a scenario where a user's profile information is required before fetching their recent transactions. Async functions enable the sequential execution of these operations, ensuring a clear and logical flow while avoiding unnecessary delays in processing.

    In this kind of scenarios, you need to choose from several clumsy solutions as below: 

    • The `.then` Christmas Tree:

    When chaining multiple asynchronous operations using `.then` callbacks, the resulting nested structure, often referred to as the "Christmas tree" or "callback hell," can be challenging to read and maintain. 

    fetchData('https://api.example.com/endpoint1')
     .then(data1 => {
     return fetchData('https://api.example.com/endpoint2')
     .then(data2 => {
     // Process data1 and data2 here
     });
     });

    Explanation: This example illustrates a common issue known as the "Christmas tree" or "callback hell," where nested .then callbacks create a visually confusing structure. Async functions can be used to flatten the code, providing a more readable and maintainable alternative.

    •  Moving to a Higher Scope:

    Moving variables to a higher scope outside async functions simplifies code structure and avoids unnecessary nesting. In scenarios where data from asynchronous operations needs to be shared across different parts of the code, declaring variables at a higher scope enhances readability.

    // Function to simulate an asynchronous operation
    function fetchData() {
     return new Promise((resolve) => {
     setTimeout(() => {
     resolve('Data from async operation');
     }, 1000);
     });
    }
    let fetchedData;
    // Fetch data using .then and move to a higher scope
    fetchData()
     .then((data) => {
     // Move data to a higher scope
     fetchedData = data;
     // Additional processing or logic here
     console.log('Fetched Data:', fetchedData);
     })
     .catch((error) => {
     console.error('Error fetching data:', error.message);
     });

    Explanation:

    - The `fetchData` function simulates an asynchronous operation by returning a Promise.

    - The variable `fetchedData` is declared at a higher scope outside the `.then` block.

    - The data fetched asynchronously is assigned to `fetchedData` within the `.then` block, making it accessible for further processing outside the asynchronous operation.

    • The Unnecessary Array:

    The Unnecessary Array pattern addresses the situation where multiple asynchronous operations need to be performed concurrently. 

    function executeAsyncTask () {
     return functionA()
     .then(valueA => {
     return Promise.all([valueA, functionB(valueA)])
     })
     .then(([valueA, valueB]) => {
     return functionC(valueA, valueB)
     })
     }

    Explanation: Each operation is chained using .then to ensure they execute in order. The use of Promise.all in the first .then block allows parallel execution of functionA and functionB. The final result is the outcome of functionC.

    •  Write a Helper Function:

    Encapsulating asynchronous logic in helper functions promotes code modularity and reusability. For instance, consider a scenario where various parts of an application need to fetch and process data from an external API. By creating an async helper function, like `fetchDataAndProcess`, developers can centralize the logic, reducing redundancy and simplifying maintenance across the codebase, still the code is not that straighforward to understand.

    // Function to simulate an asynchronous operation
    function fetchData() {
     return new Promise((resolve) => {
     setTimeout(() => {
     resolve('Data from async operation');
     }, 1000);
     });
    }
    // Helper function using .then for intermediate values
    function processIntermediateData(data) {
     return fetchData()
     .then((additionalData) => {
     // Process and combine the intermediate and additional data
     return `${data} + ${additionalData}`;
     });
    }
    // Usage of the helper function
    processIntermediateData('Initial Data')
     .then((finalResult) => {
     console.log('Final Result:', finalResult);
     })
     .catch((error) => {
     console.error('Error:', error.message);
     });

    Explanation:

    The `fetchData` function simulates an asynchronous operation returning a Promise.

    The `processIntermediateData` function takes an initial data parameter, fetches additional data asynchronously using `.then`, and processes and combines the intermediate and additional data.

    The usage demonstrates how to use the helper function, chain `.then` for handling the final result, and handle errors with `.catch`.

    With Async Await in Node JS, the code becomes pleasing to the eye and simple to understand.

    async await in Node Js handles intermediate values better than .then callback and promises in JavaScript by providing a more natural and synchronous-looking syntax. With async and await in Node JS, the code structure is linear, making it easier to follow and maintain, especially when dealing with multiple asynchronous operations. It eliminates callback nesting, reducing the likelihood of callback hell. This leads to more readable and expressive code, enhancing developer productivity and code quality.

    // Function to simulate an asynchronous operation
    function fetchData() {
     return new Promise((resolve) => {
     setTimeout(() => {
     resolve('Data from async operation');
     }, 1000);
     });
    }
    
    // Main function using async await node js example for intermediate values
    async function main() {
     try {
     // Step 1: Fetch initial data
     const initialData = 'Initial Data';
    
     // Step 2: Fetch additional data asynchronously
     const additionalData = await fetchData();
    
     // Step 3: Process and combine the intermediate and additional data
     const finalResult = `${initialData} + ${additionalData}`;
    
     // Step 4: Output the final result
     console.log('Final Result:', finalResult);
     } catch (error) {
     console.error('Error:', error.message);
     }
    }
    
    // Call the main function
    main();

    Explanation:

    The `main` function is marked as `async` to allow the use of `await` within the function.

    The initial data is set in Step 1.

    In Step 2, `await fetchData()` is used to asynchronously fetch additional data.

    Steps 3 and 4 process and output the final result.

    This approach directly uses async await in Node Js within the main function without employing a separate helper function. It demonstrates the sequential and readable nature of async await node when handling multiple asynchronous steps.

    Handling Multiple Parallel Requests with Async Await in Node js

    Handling multiple parallel requests also becomes a breeze when we utilize async and await in Node js.

    async function fetchMultipleData() {
     try {
     // Initiate multiple asynchronous requests concurrently
     const [userData, postList, commentList] = await Promise.all([
     fetchData('https://api.example.com/users'),
     fetchData('https://api.example.com/posts'),
     fetchData('https://api.example.com/comments')
     ]);
    
     // Process the obtained data
     console.log('User Data:', userData);
     console.log('Post List:', postList);
     console.log('Comment List:', commentList);
    
     // Additional processing or business logic can be applied here
     } catch (error) {
     console.error('Error fetching data:', error.message);
     }
    }
    
    // Helper function to simulate an asynchronous nodejs data fetch
    function fetchData(url) {
     return new Promise((resolve, reject) => {
     setTimeout(() => {
     // Simulating a successful API call
     resolve({ data: `Data from ${url}` });
     }, 1000);
     });
    }
    
    // Execute the parallel requests function
    fetchMultipleData();

    Explanation:

    The fetchMultipleData function concurrently initiates three async Node.js requests using Promise.all, awaiting results from the fetchData function, which simulates async operations. Upon resolution, data is destructured and processed. Error handling using a try-catch block ensures robust error logging.

    This pattern ensures that multiple asynchronous operations are executed concurrently, significantly improving the overall performance of the application. It's crucial for scenarios where waiting for one request before initiating the next would introduce unnecessary delays.

    KnowledgeHut brings you an amazing Full Stack Developer course with placement guarantee that would help you learn and get placed in leading product and software based companies as a full stack developer

    Methods to Iterate Arrays with Async Await in Node js

    Methods to Iterate Arrays with Async Await in Node js

    Array iteration methods provide a concise and expressive way to work with arrays. When combined with async and await in node js, these methods offer a clean approach to asynchronously process array elements. Let's explore an example using the `map`, filter and reduce function to fetch data from multiple API endpoints concurrently.

    // Function to simulate an asynchronous nodejs operation
    function fetchData(item) {
     return new Promise((resolve) => {
     setTimeout(() => {
     resolve(`Processed ${item}`);
     }, Math.random() * 1000); // Simulating variable asynchronous delay
     });
    }
    
    // Array of items to process asynchronously
    const itemsToProcess = [1, 2, 3, 4, 5];
    
    

    map: map is a versatile array method in JavaScript that transforms each element of an array based on a provided callback function. It creates a new array with the results, allowing developers to easily modify and derive values from the original array without changing its structure.

    // Using async await in nodejs with array iteration map
    async function processArrayAsync() {
     try {
     // Using map to process each item asynchronously
     const mappedResults = await Promise.all(
     itemsToProcess.map(async (item) => {
     return fetchData(item);
     })
     );
    
    console.log('Mapped Results:', mappedResults);

    filter: The filter array method selectively extracts elements from an array that meet a specified condition defined by a callback function. It returns a new array containing only those elements that satisfy the condition, offering an elegant way to narrow down data based on specific criteria.

    // Using filter to select specific items asynchronously
     const filteredResults = await Promise.all(
     itemsToProcess.filter(async (item) => {
     const data = await fetchData(item);
     return data.includes('Processed 3'); // Select items containing 'Processed 3'
    })
     );
    console.log('Filtered Results:', filteredResults);

    reduce: reduce is a powerful array method that iterates through the elements of an array, accumulating them into a single value. The process involves applying a callback function that handles the aggregation logic, enabling developers to derive complex results, such as sums, averages, or concatenated strings, from the elements of an array.

    // Using reduce to accumulate results asynchronously
     const reducedResult = await itemsToProcess.reduce(async (accumulator, item) => {
     const accumulatedValue = await accumulator;
     const data = await fetchData(item);
     return `${accumulatedValue} | ${data}`;
     }, '');
     console.log('Reduced Result:', reducedResult);
     } catch (error) {
     console.error('Error:', error.message);
     }
    }
    // Call the async function
    processArrayAsync();
    
    

    Explanation:

    The `fetchData` function simulates an asynchronous operation on each item in the array.

    `map` is used to process each item asynchronously and gather the results into an array.

    `filter` is used to asynchronously filter items based on a condition.

    `reduce` is used to accumulate results asynchronously.

    These array iteration methods with async await in nodejs allow you to work with asynchronous operations on each element of an array in a concise and readable manner.

    Rewriting Callback-Based Node.js Applications with Async and Await in Node js

    Rewriting callback-based Node.js applications using `async and await in Node JS` is a transformative process that significantly enhances code readability and maintainability. Callback-based code often leads to callback hell, making it challenging to follow the flow of execution and manage asynchronous tasks. The adoption of `async await node` simplifies this complexity and provides a more synchronous-like structure, leading to cleaner and more efficient code.

    Before Rewriting: Callback-Based Code

    function fetchDataFromAPI(callback) {
     externalAPI.getData((error, data) => {
     if (error) {
     callback(error);
     } else {
     externalAPI.processData(data, (processingError, processedData) => {
     if (processingError) {
     callback(processingError);
     } else {
     externalAPI.saveData(processedData, (saveError) => {
     if (saveError) {
     callback(saveError);
     } else {
     callback(null, 'Data successfully saved');
     }
     });
     }
     });
     }
     });
    }

    After Rewriting: Using Async Await Node JS

    async function fetchDataFromAPIAsync() {
     try {
     const data = await externalAPI.getDataAsync();
     const processedData = await externalAPI.processDataAsync(data);
     await externalAPI.saveDataAsync(processedData);
     return 'Data successfully saved';
     } catch (error) {
     throw new Error(`Error: ${error.message}`);
     }
    }
    
    

    Explanation:

    Before Rewriting: The callback-based code involves nesting multiple callbacks, creating a pyramid structure known as callback hell, it is also difficult to have control over the flow that would risk having security concerns while working with third party APIs and functions. This makes the code harder to read, maintain, and reason about.

    After Rewriting: With `async/await`, the code becomes more linear and resembles synchronous code. Each asynchronous operation is awaited, eliminating the need for nested callbacks. Error handling is simplified using a `try-catch` block.

    Rewriting Promise-Based Code with Async Await in Node.js

    Rewriting Promise-based applications using async await in Node.js is a natural evolution that simplifies the handling of asynchronous operations, resulting in cleaner and more concise code. While Promises brought significant improvements over callback-based patterns, `async/await` takes the simplicity to the next level by providing a more synchronous-looking syntax. Let's explore the process of transforming a Promise-based application with an illustrative example.

    Before Rewriting: Promise-Based Code

    function fetchDataFromAPI() {
     return externalAPI.getData()
     .then(data => externalAPI.processData(data))
     .then(processedData => externalAPI.saveData(processedData))
     .then(() => 'Data successfully saved')
     .catch(error => {
     throw new Error(`Error: ${error.message}`);
     });
    }

    After Rewriting: Using Node Js Async Await Example

    async function fetchDataFromAPIAsync() {
     try {
     const data = await externalAPI.getDataAsync();
     const processedData = await externalAPI.processDataAsync(data);
     await externalAPI.saveDataAsync(processedData);
     return 'Data successfully saved';
     } catch (error) {
     throw new Error(`Error: ${error.message}`);
     }
    }

    Explanation:

    1. Before Rewriting: The Promise-based code employs the `then` and `catch` syntax, creating a chain of promises for sequential asynchronous operations. While promises improve readability over callbacks, the code still exhibits a certain level of verbosity.

    2. After Rewriting: The `async/await` version simplifies the structure, making it more akin to synchronous code. Each asynchronous operation is awaited, eliminating the need for chaining `.then` and improving the flow of the code.

    Handling Errors with `async and await in Node Js`

    One of the notable advantages of async await in Node js is its ability to streamline error handling in asynchronous code. This paradigm simplifies the traditionally verbose error-handling mechanisms, making the code more readable and maintainable. Let's explore how `async and await in Node Js` facilitates error handling with a practical example.

    Error Handling in Asynchronous Code (Before `async/await`):

    function fetchData(callback) {
     externalAPI.getData((error, data) => {
     if (error) {
     callback(error);
     } else {
     externalAPI.processData(data, (processingError, processedData) => {
     if (processingError) {
     callback(processingError);
     } else {
     externalAPI.saveData(processedData, (saveError) => {
     if (saveError) {
     callback(saveError);
     } else {
     callback(null, 'Data successfully saved');
     }
     });
     }
     });
     }
     });
    }

    Error Handling with async await in node js:

    async function fetchDataAsync() {
     try {
     const data = await externalAPI.getDataAsync();
     const processedData = await externalAPI.processDataAsync(data);
     await externalAPI.saveDataAsync(processedData);
     return 'Data successfully saved';
     } catch (error) {
     throw new Error(`Error: ${error.message}`);
     }
    }

    Explanation:

    Before `async/await`: The callback-based approach involves numerous nested callbacks, leading to the infamous callback hell. Error handling is interspersed throughout the code, making it less readable and prone to mistakes.

    With `async/await`: The `try-catch` block simplifies error handling in the `async/await` version. If any of the awaited Promises reject (encounter an error), the control flows directly to the `catch` block. This provides a centralized location for error handling, enhancing code clarity.

    Best Practices for Error Handling with async await in Node Js:

    • Use `try-catch` Blocks: Surround the `await` expressions with a `try-catch` block to gracefully handle errors. This ensures that errors occurring within asynchronous operations are caught and can be handled appropriately.
    • Centralized Error Handling: Place the `try-catch` block at a level where it makes sense to handle errors collectively. This might be within a specific function, route handler, or the entry point of your application.
    • Throwing Custom Errors: When catching an error, consider throwing a custom error or providing additional context in the error message. This aids in debugging and makes error messages more informative.
    • Handle Errors Asynchronously: If the error can be handled without halting the entire operation, handle it asynchronously within the `catch` block. This might involve logging the error, sending notifications, or taking other appropriate actions.
    • Return Meaningful Error Messages: When throwing errors, provide meaningful error messages that convey information about the nature of the problem. This assists developers in diagnosing issues during debugging.

    Conclusion

    Mastering async await in Node js offers a readable alternative to callback-based and Promise-based patterns, streamlining code, reducing callback hell, and simplifying error handling. In this node js async await tutorial, we explored patterns like exponential backoff and handling intermediate values, transforming asynchronous code for enhanced clarity and developer productivity. Comparing `async/await` with array iteration methods, such as `map`, `filter`, and `reduce`, demonstrated its efficiency in processing arrays. In conclusion, embracing `async/await` empowers Node.js developers to craft more maintainable, efficient code, marking a significant evolution in asynchronous programming and a valuable addition to the modern developer's toolkit.

    Also you learn further and be proficient in NodeJs, consider joining KnowledgeHut Node.js full course that incorporates basic to advanced knowledge along with practical hands on learning in NodeJs.

    Frequently Asked Questions (FAQs)

    1Can Async/Await be used outside of functions marked as async?

    No, `async/await` can only be used within functions marked with the `async` keyword. Attempting to use `await` outside such functions will result in a syntax error. The `async` keyword signals that the function contains asynchronous operations.

    2Can Async/Await be used in the browser?

    Yes, `async/await` can be used in the browser, and it is widely adopted in modern JavaScript development. It provides a more readable and efficient way to handle asynchronous operations, making it especially beneficial for tasks like fetching data from APIs, handling user interactions, and managing asynchronous events in web applications.

    3Are there any performance considerations with Async/Await?

    While `async/await` simplifies asynchronous code, it introduces a slight overhead due to the transformation of the code into Promises. In most cases, the impact on performance is negligible and outweighed by the readability and maintainability benefits. However, it's crucial to consider potential bottlenecks in performance-critical applications and evaluate whether the benefits of using `async/await` outweigh the minor performance trade-offs.

    Profile

    Darlington Gospel

    Blog Author

    I am a Software Engineer skilled in JavaScript and Blockchain development. You can reach me on LinkedIn, Facebook, Github, or on my website.

    Share This Article
    Ready to Master the Skills that Drive Your Career?

    Avail your free 1:1 mentorship session.

    Select
    Your Message (Optional)

    Upcoming Web Development Batches & Dates

    NameDateFeeKnow more
    Course advisor icon
    Course Advisor
    Whatsapp/Chat icon