The Grouparoo Blog


Three Methods for Promisifying Node Callback Functions

Tagged in Engineering Notes 
By Sean C Davis on 2021-03-25

Callback Hell

The Grouparoo application is written in JavaScript (Node). It uses the modern promise-based pattern (async/await) for reading and writing data asynchronously. And we do this a lot — we are a data sync tool!

Every once in awhile we'll come across a JavaScript library that is written around the old callback-based pattern, where the error object is the first parameter in the callback function, followed by the result.

The old way looked something like this:

doThing('theThing', function(error, result) {
  // Catch the error or do something with result ...
}

This pattern isn't compatible with the new approach where I want to wait for each asynchronous function to resolve so I can predict the order in which my code is executed.

Every once in awhile we'll come across a library that follows this old pattern. We'll have to figure out a way to make it work with our code. There are three approaches you can take in many cases:

Option #1: Find an Existing Wrapper

It's possible there is a promise-based version of the library you're looking to use.

For example, I was looking to work with the Node-based sqlite3 library and I found a package called sqlite-async.

Personally I don't love this option for two reasons:

  1. It's another layer of dependencies that you have to worry about someone keeping up (and that someone is very likely not the author of the thing you really want).
  2. It's not that difficult to do yourself (without an additional dependency).

Option #2: Wrap it Yourself

The JavaScript Promise API is well-built for you to manually wrap the callback-based functions in promises. To promisify those functions.

Take our example:

doThing('theThing', function(error, result) {
  // Catch the error or do something with result ...
}

You could wrap this in a promise-based function called doThingAsync like so:

doThingAsync(param1) {
  return new Promise((resolve, reject) => {
    doThing(param1, (error, result) => {
      if (error) return reject(error)
      return resolve(result)
    })
  })
}

Now you can run the original example like so:

const result = await doThingAsync("theThing");

This is a great method when you only need to wrap a few functions or when you want fine-grained control on the output of specific functions. For example, if you want more control over the error messages returned.

If there are a lot of functions to wrap or customize, it might be worth it to go back and look for an existing wrapper (Option #1). But don't go yet — there's a magical third option!

Option #3: Node's util.promisify()

Node has a built-in promisify utility that does this work for you.

Using this approach, we can rewrite our original example like so:

import { promisify } from "util";

const doThingAsync = promisify(doThing);
const result = await doThingAsync("theThing");

That's super simple! And that's why this is my preferred approach when it can be implemented cleanly.

Part of the reason this is so simple is also because it is opinionated. There are two gotchas that you should lookout for when using this utility:

Gotcha #1: Callback Pattern

For this to work right, the callbacks must follow a strict parameter structure. The callback functions must pass an error argument first (which is null or undefined if there is no error), and the result object second.

This is the structure I've shown in the examples here. But if the library you're dealing with has a different callback structure, you won't be able to use util.promisify() with it.

Gotcha #2: Binding Instances

When we're dealing with an instance of a class or object, we have to bind that object to promisify. Here's an example:

const instance = new Thing();
const doThingAsync = promisify(instance.doThing).bind(instance);
const result = await doThingAsync("theThing");

Stay up to date

We will let you know about our launch and new content.

Share this post

Twitter Logo