Roll your own promisify function

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:

1
2
3
someAsyncFunction(arg1, arg2, function(data) {
// do something with `data`
});

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.

1
2
3
4
5
6
7
someAsyncFunction(arg1, arg2, function(foo) {
anotherAsyncFunction(data, function(bar) {
moreAsyncWork(function(bar) {
// eventually this function runs, invoked with `bar`
});
});
});

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.

1
2
3
4
5
6
7
someAsyncFunction(arg1, arg2)
.then(function(foo) {
return anotherAsyncFunction(foo);
})
.then(function(bar) {
return moreAsyncWork(bar);
});

Here we see several improvements in readability:

  1. Flattened code indentation with less nesting.
  2. No passing functions as arguments.
  3. 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:

1
2
3
function nodeStyleFunction(arg1, arg2, function(err, data) {
// handle error, work with data
});

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 null if 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.
1
2
3
4
5
function promisify(nodeStyleFunction) {
return function() {
...
}
}

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 resolve and a reject parameter.
  • Return the Promise instance.

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:

1
2
3
4
5
6
7
function promisify(nodeStyleFunction) {
return function() {
return new Promise(function(resolve, reject) {
...
};
};
}

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.

1
2
3
4
5
...
return new Promise(function(resolve, reject) {
nodeStyleFunction();
};
...

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 null for the first parameter and the function’s result as the second. However, the promise system invokes the Promise.resolve in 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.reject method, optionally passing data.

Let’s define a skeleton for the Node-style callback while using reject and resolve to handle both scenarios:

1
2
3
4
5
6
7
var nodeStyleCallback = function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
}

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:

1
2
3
4
5
6
7
8
9
10
return new Promise(function(resolve, reject) {
var nodeStyleCallback = function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
}
nodeStyleFunction(nodeStyleCallback);
});

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 slice method to convert the array-like arguments object to a proper array.
  • push the callback onto the end of the array.
  • Invoke our nodeStyleFunction with Function.prototype.apply since it takes an array of arguments.

Here’s the entire promisify function so far, including the above steps for handling arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var promisify = function(nodeStyleFunction) {
return function() {
var args = Array.prototype.slice.call(arguments);
return new Promise(function(resolve, reject) {
args.push(function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
nodeStyleFunction.apply(null, args);
});
};
};

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fs = require('fs');
// OPEN the file
fs.open('file.txt', 'r', function(err, file) {
if (err) {
return console.err(err);
}
var buffer = new Buffer(1024);
// READ the file
fs.read(file, buffer, 0, buffer.length, 0,
function(err, bytes) {
if (err) { throw err };
// do something with the bytes
...
// CLOSE the file
fs.close(file, function(err) {
if (err) { throw err; }
});
});
});

Now it’s promisify‘s turn. Let’s start by converting the various fs functions to their promisified versions:

1
2
3
4
5
var fs = require('fs');
var openAsync = promisify(fs.open);
var readAsync = promisify(fs.read);
var closeAsync = promisify(fs.close);

And here’s the equivalent file-handling code:

1
2
3
4
5
6
7
8
9
10
11
openAsync('file.txt', 'r')
.then(function(file) {
readAsync(file, buffer, 0, buffer.length, 0);
})
.then(function(bytes) {
// do stuff with the bytes
closeAsync();
})
.catch(function(err) {
console.error(err);
});

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.