Node.js Async and Await

Async/await is the modern, most readable way to write asynchronous code in Node.js. It is built on top of Promises and allows asynchronous code to be written in a style that looks and reads almost like regular synchronous code — without the complexity of chaining .then() calls.

The async keyword is used to declare a function as asynchronous. Inside an async function, the await keyword pauses execution until a Promise resolves, and then returns the result — all without blocking the rest of the program.

The async Keyword

When a function is declared with the async keyword, it automatically returns a Promise — even if the function just returns a plain value:

async function greet() {
  return "Hello from an async function!";
}

greet().then(function(message) {
  console.log(message);
});
// Output: Hello from an async function!

The function returns a string, but because it is async, it is automatically wrapped in a resolved Promise.

The await Keyword

await can only be used inside an async function. It pauses the async function at that point and waits for the Promise to resolve, then returns the resolved value:

function fetchData() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve("Data loaded successfully!");
    }, 1000);
  });
}

async function main() {
  console.log("Fetching data...");
  const result = await fetchData();
  console.log(result);
  console.log("Done.");
}

main();

Output:

Fetching data...
Data loaded successfully!
Done.

The code inside main() looks synchronous but is actually non-blocking. While waiting for fetchData(), the Node.js event loop is free to handle other tasks.

Error Handling with try/catch

With async/await, errors are handled using standard try/catch blocks — the same way synchronous errors are caught:

function getUserById(id) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 800);
  });
}

async function showUser(id) {
  try {
    const user = await getUserById(id);
    console.log("User:", user.name);
  } catch (err) {
    console.log("Error:", err.message);
  }
}

showUser(1);   // User: Alice
showUser(99);  // Error: User not found

Sequential Async Operations

Async/await shines when multiple asynchronous operations need to happen one after another:

function step(name, delay) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(name + " completed");
    }, delay);
  });
}

async function runSteps() {
  console.log("Starting workflow...");

  const result1 = await step("Login", 500);
  console.log(result1);

  const result2 = await step("Load Dashboard", 700);
  console.log(result2);

  const result3 = await step("Fetch Reports", 400);
  console.log(result3);

  console.log("All steps completed.");
}

runSteps();

Output:

Starting workflow...
Login completed
Load Dashboard completed
Fetch Reports completed
All steps completed.

Each step waits for the previous one to finish before starting — easy to read and maintain.

Parallel Execution with Promise.all and await

When multiple tasks do not depend on each other, they can run in parallel for better performance:

async function fetchParallel() {
  const task1 = step("Task A", 1000);
  const task2 = step("Task B", 800);
  const task3 = step("Task C", 600);

  // Start all tasks and wait for all to finish
  const [r1, r2, r3] = await Promise.all([task1, task2, task3]);

  console.log(r1);
  console.log(r2);
  console.log(r3);
}

fetchParallel();

Output (all finish in ~1 second total instead of ~2.4 seconds sequentially):

Task A completed
Task B completed
Task C completed

Using Async/Await with the fs Module

The promise-based version of fs works directly with async/await:

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

async function readAndWrite() {
  try {
    // Read a file
    const data = await fs.readFile('input.txt', 'utf8');
    console.log("Read:", data);

    // Write to another file
    await fs.writeFile('output.txt', "Processed: " + data);
    console.log("Written to output.txt");

  } catch (err) {
    console.log("File error:", err.message);
  }
}

readAndWrite();

Async Functions in Loops

When using await inside a loop, be aware of whether operations should run sequentially or in parallel:

Sequential Loop (one at a time)

async function processItems() {
  const items = ["Apple", "Banana", "Cherry"];

  for (const item of items) {
    await step(item, 300); // Each waits for the previous
    console.log("Processed:", item);
  }
}

processItems();

Parallel Loop (all at once)

async function processAllParallel() {
  const items = ["Apple", "Banana", "Cherry"];

  const results = await Promise.all(
    items.map(item => step(item, 300))
  );

  results.forEach(r => console.log(r));
}

processAllParallel();

Use a regular for...of loop for sequential tasks and Promise.all with .map() for parallel tasks.

Async/Await vs Callbacks vs Promises

FeatureCallbacksPromisesAsync/Await
ReadabilityLow (callback hell)Medium (.then chains)High (looks synchronous)
Error HandlingManual checks.catch() at endtry/catch block
ChainingNested functions.then() chainsStraightforward sequential code
DebuggingHarderModerateEasiest

Key Points

  • async marks a function as asynchronous and ensures it always returns a Promise.
  • await pauses an async function until the awaited Promise resolves and returns its value.
  • Error handling in async/await uses try/catch, making it consistent with synchronous error handling.
  • Sequential async operations are written naturally one after another using await.
  • For parallel tasks, Promise.all() combined with await gives the best performance.
  • Async/await is built on top of Promises — understanding Promises first makes async/await easier to grasp.
  • Async/await is the recommended, modern approach for writing asynchronous code in Node.js.

Leave a Comment

Your email address will not be published. Required fields are marked *