Skip to content

Using Callbacks With Async/Await

3 min read

Let's face it. Sometimes you have to use an asynchronous function that uses the old callback-based approach. It might be part of a legacy codebase, or it could come from a library that hasn't had an update in over 5 years. 🤷‍♂️

Nevertheless, there's no way around it and you have to deal with it somehow.

So what do you do? Do you add messy callbacks to your code? 🙅‍♂️

What if I told you there is a way to make callback-based asynchronous functions work nicely with async/await? Not only that, but you won't have to make any changes to existing code.

I'll teach you how to change a function's signature, even when you don't have control over its implementation.

Transforming callback-based asynchronous functions

Suppose we have a getUserAddress function that's written using modern async/await. Inside this function, we're calling fetchUser which uses a callback-based approach.

When fetchUser completes, it calls the callback with the user object. Our goal is to have getUserAddress return the user's address.

async function getUserAddress(userId) {
  fetchUser(userId, (err, user) => {
    if (err) {
      console.log(err);
      return;
    }

    user.address; // we want this value
  });
}

const address = await getUserAddress(8); // to be returned here
console.log(address); // undefined

How do we accomplish that?

We can't simply return user.address inside the callback function because the return value would go to the parent function that calls the callback, which is the fetchUser function and not getUserAddress.

async function getUserAddress(userId) {
  fetchUser(userId, (err, user) => {
    if (err) {
      console.log(err);
      return;
    }

    // 🚫 doesn't work, value is returned to fetchUser function
    return user.address;
  });
}

const address = await getUserAddress(8);
console.log(address); // undefined

And returning fetchUser doesn't work either because by design the function doesn't return anything. In fact, all callback-based asynchronous functions return undefined.

async function getUserAddress(userId) {
  // 🚫 doesn't work, fetchUser doesn't return anything
  return fetchUser(userId, (err, user) => {
    if (err) {
      console.log(err);
      return;
    }

    user.address;
  });
}

const address = await getUserAddress(8);
console.log(address); // undefined

Well then, the only way to use fetchUser with async/await is if the function returns a Promise. Callback-based functions are incompatible with promises & async/await because they are different approaches to writing asynchronous code.

But how do we make fetchUser return a Promise when we don't have control over its implementation?

We wrap it! 🌯

If we wrap fetchUser inside a promise that resolves with the user value and rejects with the error object, we can await the promise and return the user's address inside getUserAddress.

async function getUserAddress(userId) {
  // Create and await a promise that rejects and resolves
  // with the callback's `err` and `user` arguments, respectively
  try {
    const user = await new Promise((resolve, reject) => {
      fetchUser(userId, (err, user) => {
        if (err) {
          reject(err);
          return;
        }

        resolve(user);
      });
    });
    return user.address;
  } catch (error) {
    console.log(error);
  }
}

const address = await getUserAddress(8);
console.log(address); // '729 Ellsworth Summit, Aliyaview'

What this does is it creates a bridge between the asynchronous callback and async/await. It translates the success and error paths inside the callback to the resolve and rejection states of the promise.

When the callback is called with an err argument the promise will reject with the error object which is then caught and logged in the catch block. If there is no error, the promise resolves with the user which is assigned to a variable.

Finally, we call getUserAddress which returns a promise, because all async functions return a promise, and await it to get the user's address.

Using a utility function

It quickly becomes tedious if you have to manually wrap all callback-based functions in your project. Luckily, Node.js comes with a utility function that does this for you.

The promisify method from the built-in util module takes a callback-based asynchronous function and returns a promise-based version. You call the promise-based function with the same parameters as you would call the callback-based function, except instead of passing in a callback you await the promise for the resolved value.

import util from "node:util";
import { fetchUser } from "../utils/api.js";

// util.promisify transforms a callback-based function
// into a promise-based equivalent
const fetchUserAsync = util.promisify(fetchUser);

async function getUserAddress(userId) {
  try {
    const user = await fetchUserAsync(userId);
    return user.address;
  } catch (error) {
    console.log(error);
  }
}

const address = await getUserAddress(8);
console.log(address); // '729 Ellsworth Summit, Aliyaview'

This is much better than what we've started with, and it works! 🎉

You've learned how to transform a callback-based asynchronous function into a function that returns a promise despite not having control over its internal implementation. There's no need to write asynchronous JavaScript code using a callback-based approach anymore.

Master Asynchronous JavaScript 🚀

Learn how to write modern and easy-to-read asynchronous code with a FREE 5-day email course.

Through visual graphics you will learn how to decompose async code into individual parts and put them back together using a modern async/await approach. Moreover, with 30+ real-world exercises you'll transform knowledge into a practical skill that will make you a better developer.

Refactoring Callbacks, a FREE 5-day email course. 30+ real-world exercises, a visual guide and 5 days, 5 lessons.

Get Lesson 1 now 👇🏼

You'll also get tips on building scalable Node.js applications about twice a month. I respect your email privacy. Unsubscribe any time.

You might also like

Why You Shouldn't Mix Promise.then() With Async/Await Syntax

Mixing Promise.then() with async/await syntax is a recipe for bugs. Here's why you should avoid it and what to do instead.
Read article

Run Concurrent Tasks With a Limit Using Pure JavaScript

Run asynchronous tasks with a concurrency limit without having to rely on an external library.
Read article

14 Linting Rules To Help You Write Asynchronous Code in JavaScript

A compiled list of linting rules to specifically help you with writing asynchronous code in JavaScript and Node.js.
Read article