Blog Cover

How to convert callbacks into promises

Author profile image
Aitor Alonso

Apr 23, 2022

Updated Oct 28, 2023

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.


I hope my article has helped you, or at least, that you have enjoyed reading it. I do this for fun and I don't need money to keep the blog running. However, if you'd like to show your gratitude, you can pay for my next coffee(s) with a one-time donation of just $1.00. Thank you!