
Introduction
We have several choices when it comes to JavaScript promise libraries. Some include a promisify feature that allows you to turn regular callback-style asynchronous functions into promises. Additionally, NPM hosts numerous Node modules that promisify functions. So why would we want to create our own?
Rolling our own version will deepen our understanding of how promisification, and ultimately promises themselves, work. Also, knowing how to write a custom variant just might come in handy when other modules don’t deliver the desired results. We’ll look at promisifying node-style callback functions, but the general principles apply other potential scenarios.
First let’s review some of the reasoning behind using promises. If you’re familiar with this pattern, skip to Building up promisify.
Callbacks and promises
Most JavaScript and Node.js developers are familiar with callbacks:
|
|
In this example, we’re passing a function into someAsyncFunction as the final argument. Why can’t we simply return the results after the function does its work? This function, like many in Node and other event-driven systems, does perform that work. However, we don’t know when it will finish and we don’t want to lock up the application’s single thread while waiting.
Instead, we pass in a callback, a function that will be invoked by someAsyncFunction at an undetermined time in the future. After that time we can work with the data, which in this case is passed into the callback as the argument data. We might even invoke more async functions within that callback, in turn specifying more callbacks to fire when subsequent operations complete.
|
|
This simple example doesn’t include logic within each callback, but you can imagine how this approach might quickly become difficult to read, debug or determine the order of execution.
Promises attempt to improve this situation by abstracting the callback pattern into something more readable.
|
|
Here we see several improvements in readability:
- Flattened code indentation with less nesting.
- No passing functions as arguments.
- Intuitive semantics using
.then()to represent event sequence.
Building up Promisify
The Node-style pattern
Now that we’ve reviewed how promises are used, let’s progressively create a factory that will take in a function and return a version that leverages promises. More specifically, we’ll transform a function that follows the node callback pattern:
|
|
This error-first argument pattern is what distinguishes node-style function callbacks. The asynchronous outer function eventually invokes the supplied callback, which takes in:
- an error argument, usually set to
nullif nothing went wrong - response data, passed in as the second argument
Foundation function
So what exactly does this function need to perform? For starters, it should:
- Take in a node-style async function.
- Return a function that follows the promise pattern.
|
|
This provides a skeleton but the function we return needs to return a promise. The exact preparation of the promise varies between libraries. We’ll use the method for native ES6 and Bluebird promises. These require the following steps:
- Instantiate a new
Promise. - Pass in a function that accepts a
resolveand arejectparameter. - Return the
Promiseinstance.
The function we pass into the Promise constructor will be run later, but the promise itself returns immediately. This object allows the user to chain methods like .then, .catch, .finally onto the it. Here’s how the instantiation will look in promisify:
|
|

Invoking the wrapped function
We’ve built the foundation for promisify, returning a promise object with the correct function wrapper. Now we need to actually invoke the function we’re wrapping.
|
|
The Node function expects any number of arguments, as long as the last one is an error-first callback. We need to pass in those arguments as well as the callback. Let’s start with the latter.
Here’s where we reconcile the gap between a node-style callback and a promise-wrapped function. Some major differences:
- On successful completion, the node function invokes the callback, passing in
nullfor the first parameter and the function’s result as the second. However, the promise system invokes thePromise.resolvein this situation, passing in the results. - The node function sends an error to the callback if something goes wrong, whereas the promise-wrapped function calls the
Promise.rejectmethod, optionally passing data.
Let’s define a skeleton for the Node-style callback while using reject and resolve to handle both scenarios:
|
|
nodeStyleCallback is the function we want to be invoked after nodeStyleFunction, the function we’re promisifying, finishes doing its async work. The function is essentially the same thing we’d pass into nodeStyleFunction as the final argument.
We can pass it directly into nodeStyleFunction like this:
|
|
Handling arguments
But there’s just one problem. We don’t know how many arguments our promisified function will take. How do we pass in a variable number of arguments along with the callback into nodeStyleFunction? In ES6 we could use rest parameters. For this ES5 example, let’s do the following:
- Gather the arguments that are being passed to the promisified function we’re returning. Use Array’s
slicemethod to convert the array-likeargumentsobject to a proper array. pushthe callback onto the end of the array.- Invoke our
nodeStyleFunctionwithFunction.prototype.applysince it takes an array of arguments.
Here’s the entire promisify function so far, including the above steps for handling arguments:
|
|
Example
Let’s see our function in action. Node’s fs file system library includes a number of asynchronous streaming operations that can be chained together by promisify. Here’s what opening and then reading a file would look with callbacks:
|
|
Now it’s promisify‘s turn. Let’s start by converting the various fs functions to their promisified versions:
|
|
And here’s the equivalent file-handling code:
|
|
I think it’s a bit easier to see what exactly is happening in each stage of the open => read => close sequence. I also like the fact that it will work with a huge class of functions. Not to mention the fact that we can fine tune it for any other async functions we’d like to promisify.