DEV Community

Cover image for Using Callbacks With Async/Await
Maxim Orlov
Maxim Orlov

Posted on • Edited on • Originally published at maximorlov.com

Using Callbacks With Async/Await

This article was originally published at https://maximorlov.com/using-callbacks-with-async-await/

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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.

Transform Callbacks into Clean Async Code! πŸš€

Tired of messy callback code? Download this FREE 5-step guide to master async/await and simplify your asynchronous code.

In just a few steps, you'll transform complex logic into readable, modern JavaScript that's easy to maintain. With clear visuals, each step breaks down the process so you can follow along effortlessly.

Refactoring Callbacks Guide Preview

Get the FREE guide

Top comments (0)