In this article, we'll learn how to update a callback-based API to support Promises as well.

First, what is an API, or application programming interface? It's sometimes referred to as a module. It is a collection of methods and variables developers can utilize in their own application.

Watch the accompanying Real Coding episode here.

Callback Functions

Many JavaScript API’s and modules provide a final parameter in their methods for something called a callback method. Most of the time you’ll see this defined as done, next, callback, or cb (abbreviation for callback). Callback functions are incredibly useful because they enable other developers get more out of your function such as error handling and asynchronous requests.

For example, an API method may produce a variety of errors and these errors, if not properly handled, can bring down an entire application. An API utilizing callback methods should be returning all Error objects as the first parameter in the callback. It is assumed that the first parameter in a callback function is always an error instance.

The function below is a simple example. Its purpose is to double the parameter x and return it via the specified callback function. error starts as null. If any of the conditional checks fail, an Error instance is assigned to error . Then if error exists (it is not null or undefined), then we do not double x and we set the variable double as null; otherwise, x is doubled and assigned to the double variable. After everything is done the function doublePositiveOnly will return the callback method with the first parameter referencing the error variable and the second parameter referencing the double variable.

function doublePositiveOnly(x, callback) {
  let error
  if ( !x )
    error = new Error('x must be defined')
  if ( typeof x !== 'number' )
    error = new Error('x must be a number')
  if ( x < 0 )
    error = new Error('x must be positive')
    
  const double = error ? null : x * 2
  
  return callback(error, double)
}

How would you use this function?

doublePositiveOnly(16, function (err, result) {
  if (err) console.error(err.message)
  console.log(result)
})

Promise Functions

Promise functions in production are easy to recognize as they utilize .then and .catch methods to return information back to the user. Nearly all callback functions can be replaced by promises, so lets rebuild our doublePositiveOnly method using promises.

function doublePositiveOnly( x ) {
  return new Promise(function (resolve, reject) {
    let error
    if ( !x )
      error = new Error('x must be defined')
    if ( typeof x !== 'number' )
      error = new Error('x must be a number')
    if ( x < 0 )
      error = new Error('x must be positive')
      
    error ? reject(error) : resolve(x * 2)
  })
}

The above function serves the exact same purpose of the callback implementation. However, this version no longer takes a callback method as a parameter. Instead it either rejects an error or resolves the result. You can use this method like so:

doublePositiveOnly(16).then(function (result) {
  // do cool stuff with the result
  console.log(result)
}).catch(function (err) {
  // oh no an error! Handle it however you please
  console.error(err.message) 
})

The readability of a Promise function is much clearer than a callback function as you can easily handle the result as well as any potential errors. There is a lot more to Promises functions I did not cover here, and I encourage you to learn as much as you can about them.

Callbacks and Promises Together

We have callbacks and we have promises. They are interchangeable and both satisfy similar needs. Now consider the scenario where we have an API that only supports callback methods. This API is downloaded 1000x times and is now running in production on countless applications. But now the maintainer wants to support Promises as well. Can they do this while also maintaining callback support? YES!

Lets look at the callback implementation of doublePositiveOnly once again, but now also with promise support:

function doublePositiveOnly(x, callback) {
  const func = this.doublePositiveOnly
  
  if ( callback === undefined ) {
    return new Promise(function (resolve, reject) {
      func(x, function (err, result) {
        err ? reject(err) : resolve(result)
      })
    })
  }
  
  let error
  if ( !x )
    error = new Error('x must be defined')
  if ( typeof x !== 'number' )
    error = new Error('x must be a number')
  if ( x < 0 )
    error = new Error('x must be positive')
  
  const double = error ? null : x * 2
  
  return callback(error, double)
}

And just like that the doublePositiveOnly method now supports promises as well. It works because first it stores the reference to the function in the func variable. Next, it checks if a callback was passed to the function. If not it returns a promise that passes down the x parameter to another doublePositiveOnly call, and it includes a callback function. This callback function either rejects or resolves the promise just like the promise-only implementation did.

What is great about this solution is you can use just about anywhere and you don’t have to edit any parts of the original function! You can see it in action in a module I recently contributed to called fastify-jwt. Both the requestVerify and replySign methods support both callbacks and promises.

If you have any questions please reach out!

You can follow me on Github and Twitter or check out my website.

Keep up the good work.

~Ethan Arrowood