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
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.