CPCODELAB
Node.js

Async Patterns: Callbacks → Promises → async/await

14 min read

Async Patterns in Node.js

Node.js async code has evolved through three eras. Understanding all three helps you read legacy codebases and write clean modern code.

1. Callbacks (the original pattern)

Early Node.js APIs use the error-first callback convention: the first argument is always an error (or null), the second is the result.

js
const fs = require("fs");

fs.readFile("data.txt", "utf8", function(err, data) {
  if (err) {
    return console.error("Error:", err.message);
  }
  fs.writeFile("output.txt", data.toUpperCase(), function(err) {
    if (err) {
      return console.error("Write error:", err.message);
    }
    console.log("Done!");
  });
});

2. Promises

Promises chain .then() and .catch() to avoid deep nesting. You can also convert callback-based functions using util.promisify.

js
const { promisify } = require("util");
const fs = require("fs");

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

readFile("data.txt", "utf8")
  .then(function(data) {
    return writeFile("output.txt", data.toUpperCase());
  })
  .then(function() {
    console.log("Done!");
  })
  .catch(function(err) {
    console.error("Error:", err.message);
  });

3. async/await (recommended)

async/await is syntactic sugar over Promises. It makes async code read like synchronous code, improving readability significantly. Mark a function as async and await any Promise inside it.

js
const fs = require("fs/promises");

async function processData() {
  try {
    const data = await fs.readFile("data.txt", "utf8");
    await fs.writeFile("output.txt", data.toUpperCase());
    console.log("Done!");
  } catch (err) {
    console.error("Error:", err.message);
  }
}

processData();

Use Promise.all([p1, p2, p3]) to run multiple async operations in parallel instead of awaiting them sequentially. This can dramatically reduce total wait time.

Ready to test yourself?
Practice Node.js— quiz & coding exercises