Async/Await

Node.js Async/Await

While Promises (with .then() and .catch()) are a huge improvement over callbacks, they can still become slightly difficult to read when dealing with multiple interconnected asynchronous operations.

Enter Async/Await. Introduced in modern JavaScript (ES8), async/await is "syntactic sugar" built on top of Promises. It allows you to write asynchronous, non-blocking code that looks and feels like traditional synchronous code.


1. The async Keyword

To use async/await, you must first declare a function with the async keyword.

When you place async before a function, it does two things:

  1. It allows you to use the await keyword inside that function.
  2. It automatically transforms the function's return value into a Promise.

The async keyword

// Declaring an async function
async function greetUser() {
  return "Hello, Node.js Developer!";
}

// Because it's async, it returns a Promise. We can use .then() greetUser().then(message => { console.log(message); // Outputs: Hello, Node.js Developer! });


2. The await Keyword

The true power comes with the await keyword. You can place await in front of any Promise.

It tells JavaScript to pause the execution of that specific async function until the Promise resolves or rejects. Meanwhile, the rest of your Node.js application (outside the function) keeps running—the event loop is never blocked!

Using await with a Promise

// A dummy promise that takes 2 seconds to resolve
function fetchServerData() {
  return new Promise(resolve => {
    setTimeout(() => resolve("Data retrieved successfully!"), 2000);
  });
}

async function processData() { console.log("1. Requesting data...");

// The function pauses here until fetchServerData is done const result = await fetchServerData();

console.log("2.", result); console.log("3. Processing complete."); }

processData();

Notice how there are no .then() callbacks! The result of the Promise is directly assigned to the result variable. The code reads top-to-bottom sequentially, making it incredibly intuitive.


3. Error Handling with try / catch

Since async/await removes the need for .catch() chains, how do we handle errors if a Promise rejects?

We use the standard JavaScript try...catch block. This is the exact same way you handle errors in synchronous code, which makes async/await highly consistent.

const fs = require('fs').promises;

async function readConfig() { try { // Try to read a file const fileData = await fs.readFile('config.json', 'utf8'); const config = JSON.parse(fileData); console.log("Database Host:", config.host); } catch (error) { // If reading the file fails, or JSON.parse fails, the code jumps here console.error("Failed to load configuration:"); console.error(error.message); } }

readConfig();

By wrapping your await calls inside a try block, you ensure that any rejected Promise is immediately caught in the catch block, preventing application crashes.


4. Comparing Promises vs. Async/Await

Let's look at a side-by-side comparison of fetching a user profile using traditional Promises versus async/await.

Traditional Promise Chain:

function getUserDetails() {
  fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log("User Posts:", posts))
    .catch(err => console.error("Error:", err));
}

With Async/Await:

async function getUserDetails() {
  try {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    console.log("User Posts:", posts);
  } catch (err) {
    console.error("Error:", err);
  }
}

For developers, the async/await approach drastically reduces cognitive load.


Best Practices


Exercise

?

Which JavaScript keyword must be used to declare a function before you can use await inside of it?