How to Convert an Existing Callback API to Promises in JavaScript?

Callbacks are commonly used in JavaScript for asynchronous operations. However, working with callbacks can sometimes lead to callback hell, making the code difficult to read and maintain. Promises provide a cleaner and more structured way to handle asynchronous operations in JavaScript. In this article, we will learn how to convert an existing callback API to promises.

Understanding Different Callback API Formats

Before we dive into converting callback APIs to promises, let's first understand the different callback API formats.

1. DOM Load or One-time Event

In this format, a callback function is assigned to an event handler, such as window.onload. The callback function is executed once the event is triggered.

window.onload = function() {
  // Code to be executed when the DOM is loaded
}

2. Plain Callback

In this format, a function accepts a callback function as a parameter. The callback function is executed when a certain condition or change occurs.

function request(onChangeHandler) {
  // Code to perform request
  onChangeHandler();
}
request(function() {
  // Code to be executed when the request is completed
})

3. Node-style Callback ("nodeback")

This format is commonly used in Node.js APIs. A function accepts a callback function as the last parameter, which is called with an error object as the first argument (if an error occurred) and the result as the second argument.

function getStuff(data, callback) {
  // Code to get the stuff
  if (errorOccurred) {
    callback(new Error('Error occurred'), null);
  } else {
    callback(null, result);
  }
}
getStuff("dataParam", function(err, data) {
  // Code to be executed with the data or handle the error
})

4. Library with Node-style Callbacks

In this format, an API contains multiple functions with node-style callbacks. Each function has its own callback, and the flow continues with nested callbacks.

API.one(function(err, data) {
  if (err) {
    // Handle error
  } else {
    API.two(function(err2, data2) {
      if (err2) {
        // Handle error
      } else {
        API.three(function(err3, data3) {
          // Code to be executed with the data or handle the error
        });
      }
    });
  }
});

Converting Callback APIs to Promises

To convert the existing callback API to promises, we can use the following strategies:

1. Promisify Individual Functions

In this approach, we create a wrapper function that converts a callback-based function to a promise-based function using the Promise constructor.

function promisify(callbackBasedFunction) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      callbackBasedFunction(...args, (err, result) => {
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      });
    });
  };
}

// Usage
const promisifiedGetStuff = promisify(getStuff);
promisifiedGetStuff('dataParam')
  .then(data => {
    // Code to be executed with the data
  })
  .catch(error => {
    // Handle the error
  });

2. Promisify Entire Libraries

If the API consists of multiple functions with node-style callbacks, we can promisify the entire library using a library like bluebird or util.promisify in Node.js.

Using Bluebird

const Promise = require('bluebird');

const promisifiedAPI = Promise.promisifyAll(API);
promisifiedAPI.oneAsync()
  .then(data => {
    return promisifiedAPI.twoAsync();
  })
  .then(data2 => {
    return promisifiedAPI.threeAsync();
  })
  .then(data3 => {
    // Code to be executed with the data3
  })
  .catch(error => {
    // Handle the error
  });

Using util.promisify in Node.js

const util = require('util');
const getStuffPromise = util.promisify(getStuff);

getStuffPromise('dataParam')
  .then(data => {
    // Code to be executed with the data
  })
  .catch(error => {
    // Handle the error
  });

3. Promisify Using Third-party Libraries

There are also third-party libraries available that can help with converting callback APIs to promises, such as es6-promisify and es6-promisify-all.

const promisify = require('es6-promisify');

const promisifiedGetStuff = promisify(getStuff);
promisifiedGetStuff('dataParam')
  .then(data => {
    // Code to be executed with the data
  })
  .catch(error => {
    // Handle the error
  });

Conclusion

Converting callback APIs to promises allows for cleaner and more readable code, avoiding callback hell. By using techniques like function wrapping and library promisification, we can easily work with existing callback APIs in a promises-based manner. Promises provide a more structured and intuitive way to handle asynchronous operations in JavaScript.