How to convert callbacks into promises
3 min read
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.