Recently I had to integrate a very old library in a current project. That library was using callbacks instead of promises to handle async operations, and I didn’t want to mess with that, as promises outperform callbacks in all aspects, and also our entire code base was designed with promises and async / await.
Then, I had to convert the callback-based API to the new promise-based API. At first, I didn’t remember how to do that. I thought there was a method in the Promise object to easily allow this, but it was not the case. So after a memory refresh, I ended up with a few ways of converting callbacks to promises. I want to write about it here, so it serves both for me as a future reminder, and for others who might have the same problem currently.
Best option: util.promisify
Callbacks in node has always the same shape. They are passed into functions as the last argument, and they consist of a function with a least two parameters, err and data, in this order. For example, lets see fs.readFile:
const fs = require("fs")
const callback = (err, data) => {
if (err) {
// handle error
} else {
// handle data
}
}
fs.readFile("file.txt", callback)Callbacks like this can be easily transformed into promises, using the util.promisify function from the standard node util library:
const fs = require("fs")
const util = require("util")
const readFilePromise = util.promisify(fs.readFile)Then, we are able to use await to handle the promise:
try {
const file = await readFilePromise("file.txt")
} catch (err) {
// handle error
}But remember, this only works with callbacks with the (err, data) => {} signature, and when they are passed as the last argument to the function that uses them.
What if we cannot use util.promisify? Writing our own promise
To convert a callback into a promise, we need to wrap the callback in a function that returns a promise. Take a look at the following code:
const readFilePromise = (...args) => {
return new Promise((resolve, reject) => {
fs.readFile(...args, (err, data) => {
if (err) return reject(err)
resolve(data)
})
})
}I created a new function, readFilePromise, that returns a new promise that wraps the call to fs.readFile. The wrapped function receives the same arguments as the original, thanks to the spread operator ...args. Then, as a callback for the function, I pass a simple anonymous function that rejects the promise if there is an error, and resolves it with the data otherwise.
Now, we can use the new created function as before:
try {
const file = await readFilePromise("file.txt")
} catch (err) {
// handle error
}Bonus: callbacks with multiple arguments
Let’s suppose we have a function that uses a callback that receive multiple arguments, and we want to convert it to a promise.
generateRandomNumbers(options, (err, num1, num2) => {
if (err) {
// handle error
} else {
// handle data (num1 and num2)
}
})We cannot provide multiple arguments to a resolving promise, as promises only return one argument.
// This doesn't work!!!
const generateRandomNumbersPromise = (...args) => {
return new Promise((resolve, reject) => {
generateRandomNumbers(...args, (error, num1, num2) => {
if (err) return reject(err)
// You can't send two arguments into resolve
resolve(num1, num2)
})
})
}If we want to return multiple arguments, we need to use an array or an object. We can use destructuring later on to get the values separately.
// Using arrays
resolve([num1, num2])
// Destructuring
const [num1, num2] = await generateRandomNumbersPromise(/* ... */)// Using objects
resolve({ num1, num2 })
// Destructuring
const { num1, num2 } = await generateRandomNumbersPromise(/* ... */)That’s it! With these simple steps, you can easily improve the readability and maintainability of any node and JavaScript / TypeScript code.

