Course contentsShow
← Node.js
Lesson 234 of 1,9803. Asynchronous Patterns and Flow ControlFree lesson

Mixing Callbacks and Promises

You will create hybrid architectures that leverage the ubiquity of callbacks with the ergonomics of modern promises.

In this lesson, you will learn how to create "bridge" functions that allow your modern Promise-based logic to communicate seamlessly with legacy callback-based APIs.

In many real-world Node.js projects, you’ll encounter older libraries that rely exclusively on the "error-first callback" pattern (where the function receives a callback as its last argument). While you prefer async/await for your own flow control, you cannot simply ignore these legacy tools. Mixing them requires a wrapper that "promisifies" the callback behavior.

The Wrapper Pattern

To bridge the gap, you create a function that returns a new Promise. Inside the constructor, you execute the callback-based function and resolve or reject the promise based on the callback's results.

Consider this legacy function that reads a file:

// A callback-style legacy function
function legacyReadFile(path, callback) {
  // Simulating an operation
  const data = "Hello World";
  callback(null, data); // Standard (err, result) signature
}

You can wrap this to use it with await:

function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    legacyReadFile(path, (err, data) => {
      if (err) return reject(err);
      resolve(data);
    });
  });
}

// Now you can use it in an async flow!
async function run() {
  const content = await readFileAsync('file.txt');
  console.log(content);
}

Why this matters

By isolating the conversion logic into a wrapper, you protect the rest of your application from "callback hell." You essentially create a hybrid architecture: the core of the legacy module remains untouched, but your local logic benefits from the clean, linear ergonomics of modern Promises.

When you encounter an asynchronous operation that doesn't return a Promise, treat it as a task to be wrapped immediately. Once wrapped, the rest of your control flow—including try/catch error handling and sequential execution—becomes significantly more predictable and readable. You aren't changing the library; you are simply providing it with a modern interface that fits into your current architectural standards.

Key Takeaway: Always wrap legacy callback functions inside a new Promise constructor to unify your codebase under the modern async/await pattern.