Learn how to convert old Node.js callback-based function to new and shiny promised-based functions.
Originally posted on www.grouparoo.com.
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:
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:
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!
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:
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.
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");