To promise or to callback? That is the question...

Support both callbacks and promises in async modules for maximum flexibility

You are building the next cool JavaScript or Node.js module with a lot of asynchronous functions and you are very happy about it. At some point a terrible doubt assaults you:

Should my API offer support for callbacks or should it be promise based?

In this article we are going to show a very simple way to add support for both promises and callbacks in our asynchronous modules, this way we can make everyone happy and our libraries much more flexible.

The problem

Promises can be used as a nice replacement for callbacks, that’s a well know fact in the JavaScript world today. Promises turn out to be very useful in making our code more readable and easy to reason about. But while promises bring many advantages, they also require the developer to understand many non-trivial concepts in order to use them correctly and proficiently. For this and other reasons, in some cases it might be more practical to prefer callbacks over promises.

Now let’s imagine for a moment that we want to build a public library that performs asynchronous operations. What do we do? Do we create a callback oriented API or a promise oriented one? Do we need to be opinionated on one side or another or there are ways to support both and make everyone happy?

There are at least 2 approaches to face this question, let’s see how they work!

##1. The “I don’t fu**in’ care” approach

I don't care gif animation Judy Garland

The first approach, used for instance by libraries like request, redis and mysql as well as all the node native async functions, consists in offering a simple API based only on callbacks and leave the developer the option to promisify the exposed functions if needed. Some of these libraries are a bit more elaborated and they provide helpers to be able to promisify all the asynchronous functions they offer at once, but the developer still needs to someway “convert” the exposed API to be able to use promises. That’s why I believe this approach feels a bit rude like:

Do you want to use Promise? I don’t care, it’s your problem… just promisify what you want and leave me alone!

There are a number of modules out there that can help to promisify a callback based function. The first two that come in my mind are Bluebird with its Promise.promisify() method and es6-promisify which adopts ES2015 promises.

Let’s see a quick example of this approach with es6-promisify. In the following snippet of code we are going to promisify the native fs.readFile() function:

'use strict'

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

let readFile = promisify(fs.readFile)

readFile('last_action_hero.txt', 'utf-8')
  .then(content => console.log(content))
  .catch(err => console.error(err))

The example is very straightforward, we just have to call promisify(fs.readFile) to obtain a promisified version of the readFile function. As we might expect we can invoke this function without passing the callback argument and we get a Promise object as output, so we can immediately use it and call the handy then and catch methods.

Unfortunately the new Promise implementation of ES2015 does not offer a built-in promisify mechanism (yet…).

2. The “No strong feelings” way

I have no strong feeling either way futurama gif animation

This approach is more transparent and I would say more… “polite”! It is also based on the concept of offering a simple callback oriented API, but it makes the callback argument optional. Whenever the callback is passed as an argument the function will behave normally executing the callback on completion or on failure. Instead when the callback is not passed to the function, it will immediately return a Promise object.

This approach effectively combines callbacks and promises in a way that allows the developer to choose at call time what interface to adopt, without any need to promisify the function in advance. Many libraries like mongoose and sequelize are supporting this approach.

Let’s see a simple implementation of this approach with an example. Let’s assume we want to implement a dummy module that executes divisions asynchronously:

module.exports = function asyncDivision(dividend, divisor, cb) {
  return new Promise((resolve, reject) => {
    // [1]

    process.nextTick(() => {
      if (
        typeof dividend !== 'number' ||
        typeof divisor !== 'number' ||
        divisor === 0
      ) {
        let error = new Error('Invalid operands')
        if (cb) {
          return cb(error)
        } // [2]
        return reject(error)
      }

      var result = dividend / divisor
      if (cb) {
        return cb(null, result)
      } // [3]
      return resolve(result)
    })
  })
}

The code of the module is very simple, but there are some details (marked with a number in square brackets) that are worth to be underlined:

  1. First, we return a new promise created using the ES2015 Promise constructor. We define the whole logic inside the function passed as argument to the constructor.
  2. In case of error, we reject the promise, but if the callback was passed at call time we also execute the callback to propagate the error.
  3. After we calculate the result we resolve the promise, but again, if there’s a callback, we propagate the result to the callback as well.

Now to complete the example, let’s see now how we can use this module with both callbacks and promises:

// callback oriented usage
asyncDivision(10, 2, (error, result) => {
  if (error) {
    return console.error(error)
  }
  console.log(result)
})

// promise oriented usage
asyncDivision(22, 11)
  .then(result => console.log(result))
  .catch(error => console.error(error))

It should be clear that with very little effort, the developers who are going to use our new module will be able to easily choose the style that best suits their needs without having to introduce an external “promisification” function whenever they want to leverage promises.

A little caveat and an alternative implementation

As Amar Zavery pointed out in a comment here, this approach is not perfect.

If we use the function with the callback approach, we still have created and returned a promise that behaves in an inconsistent way (never resolved, nor rejected). Let’s make this clear with an example:

asyncDivision(2, 0, console.log)
  .then(() => console.log(`Promise resolved`))
  .catch(() => console.error(`Promise rejected`))

In this example, we are passing a callback (console.log) to our function but also using the returned promise to print some extra information. With our implementation of asyncDivision either the then block and the catch block are never executed because the function is interrupted as soon as the callback is used.

This is most of the time negligible because is very unlikely that somebody will be using the function with this mixed async approach (we could even argue that doing so would be a bad practice).

Anyway, we can get rid of this issue by re-implementing our async function as follows:

module.exports = function asyncDivision(dividend, divisor, cb) {
  // internal implementation, callback based
  function _asyncDivision(dividend, divisor, cb) {
    process.nextTick(() => {
      if (
        typeof dividend !== 'number' ||
        typeof divisor !== 'number' ||
        divisor === 0
      ) {
        return cb(new Error('Invalid operands'))
      }

      return cb(null, dividend / divisor)
    })
  }

  if (cb) {
    return _asyncDivision(dividend, divisor, cb)
  }

  // optional promisification, only if no callback
  return new Promise((resolve, reject) =>
    _asyncDivision(
      dividend,
      divisor,
      (err, result) => (err ? reject(err) : resolve(result))
    )
  )
}

This code generates a promise only if no callback is used. So if we provide a callback as the last argument, and then we try to use catch or then on the returning value, we will get an explicit runtime error:

  • TypeError: Cannot read property 'then' of undefined or
  • TypeError: Cannot read property 'catch' of undefined

While this approach is probably more correct, it is also more verbose and, if it becomes a recurrent practice in your development process, you are better off using some promisification library.

Conclusion

As usual I hope this article has been useful and that it will ignite some interesting conversation.

I think from the tone of the article it’s quite clear that I prefer to opt for the “polite” approach, but I am very curious to know what are your opinions about this topic and if you are an “I don’t care” or a “whatever” person when you create your asynchronous functions in your modules.

Let me know your thoughts in the comments.

Until next time! :)

Sharing is caring!

If you got value from this article, please consider sharing it with your friends and colleagues.

Found a typo or something that can be improved?

In the spirit of Open Source, you can contribute to this article by submitting a PR on GitHub.

You might also like

Cover picture for a blog post titled Migrating from Gatsby to Astro

Migrating from Gatsby to Astro

This article discuss the reason why I wanted to migrate this blog from Gatsby to Astro and the process I followed to do it. Plus a bunch of interesting and quirky bugs that I had to troubleshoot and fix along the way.

Calendar Icon

Cover picture for a blog post titled Middy 1.0.0 is here

Middy 1.0.0 is here

The middleware framework Middy reached version 1.0, bringing middleware capabilities to AWS Lambda. This allows cleaner handler code by extracting cross-cutting concerns into reusable middleware.

Calendar Icon

Cover picture for a blog post titled Lean NPM packages

Lean NPM packages

Learn how to configure NPM packages to publish only the files needed by users, avoiding bloating node_modules folders.

Calendar Icon

Cover picture for a blog post titled Fastify and Preact for quick web app prototyping

Fastify and Preact for quick web app prototyping

This article shows how to quickly build web app prototypes using Fastify for the backend API and Preact for the frontend UI. It also covers how to dockerize the app for easy sharing. Key points are the plugin architecture of Fastify, the lightweight nature of Preact, and the use of htm for defining UI without transpilation.

Calendar Icon

Cover picture for a blog post titled JavaScript iterator patterns

JavaScript iterator patterns

This article explores different ways to create iterators and iterable values in Javascript for dynamic sequence generation, specifically using functions, iterators, iterables and generators. It provides code examples for implementing the Fibonacci sequence with each approach.

Calendar Icon

Cover picture for a blog post titled Emerging JavaScript pattern: multiple return values

Emerging JavaScript pattern: multiple return values

This article explores how to simulate multiple return values in JavaScript using arrays and objects. It covers use cases like React Hooks and async/await error handling. The pattern enables elegant APIs but has performance implications.

Calendar Icon