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.

ES6 arrow functions and lexical this

Let’s take a look at one of the quirkiest behaviors in JavaScript. We’ll start by examining the legacy version of the language’s interpretation of the keyword this. Then we’ll consider some new behavior introduced by ES6’s arrow functions. How are they different? Which approach is more intuitive and/or useful? Read on for insight!

Prior to ECMAScript 2015 (a.k.a. ES6), JavaScript used an uncommon approach to the evaluation of this. It made object-orientation in a functional language difficult. For object-oriented design, this serves a critical purpose in creating objects that display polymorphism. The ECMAScript 5.1 specification interprets so-called ThisBinding in a manner often described as confusing and unintuitive.

ThisBinding is one of several execution contexts employed by the JavaScript interpreter to bind values to symbols. When passing functions as callbacks to other functions, the value of this at runtime might surprise you. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Cupcake(type, frosting) {
this.type = type;
this.frosting = frosting;
this.announce = function() {
console.log('And now, the moment you\'ve been waiting for! Presenting...');
setTimeout(this._present, 1500);
};
this._present = function() {
console.log('...a ' + this.type +
' cupcake with ' + this.frosting + ' frosting');
};
}
var chocolateCupcake = new Cupcake('chocolate', 'buttercream');
chocolateCupcake.announce();

Everything looks like it should work. The announce() method is going to queue up some code for 1.5 seconds in the future. The function it passes to setTimeout refers to some information about the cupcake. These attributes are stored in that instance of the Cupcake object. The helper function _present() is defined in the same scope as the member variables this.type and this.frosting. So why do we get the following output?

1
2
And now, the moment you've been waiting for! Presenting...
...a undefined cupcake with undefined frosting

The problem is that when present() eventually goes off, the value of this is not this cupcake instance, even though it really looks like it should be. After all, we’re defining _present() in the same block of code where we set the variables this.type and this.frosting. In truth the value of this is decided later on, in the current execution context in which it was invoked. In the case of _present(), when it’s invoked 1.5 seconds later, we’re actually in the global scope. In that case, this refers to the global object window.

Options for ThisBinding

closure

So how can we make the keyword this resolve to the thing we actually want and not window? To write event-driven, asynchronous code we need our callbacks to know about the context in which they were defined, about the objects involved in the event. If this were just like any other variable, we wouldn’t have anything to worry about. In fact, assigning the value of this to a variable and then using that variable inside the callback provides one possible solution. The closure feature of the language, part of lexical scoping, makes this possible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Cupcake(type, frosting) {
this.type = type;
this.frosting = frosting;
var self = this;
this.announce = function() {
console.log('And now, the moment you\'ve been waiting for! Presenting...');
setTimeout(this._present, 1500);
};
this._present = function() {
console.log('...a ' + self.type +
' cupcake with ' + self.frosting + ' frosting');
};
}
var chocolateCupcake = new Cupcake('chocolate', 'buttercream');
chocolateCupcake.announce();
1
2
And now, the moment you've been waiting for! Presenting...
...a chocolate cupcake with buttercream frosting

the bind method

Function.prototype.bind allows us to use a specialized version of the function that bakes in arguments and the this binding. When it’s invoked, the arguments are passed into the function as per usual. Also, the function is called as if it were invoked in the same context as the scope in which it was defined before being passed as a callback. Here’s an example of bind in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
this.announce = function() {
console.log('And now, the moment you\'ve been waiting for! Presenting...');
setTimeout(this._present.bind(this), 1500);
};
this._present = function() {
console.log('...a ' + this.type +
' cupcake with ' + this.frosting + ' frosting');
};
}
...
1
2
And now, the moment you've been waiting for! Presenting...
...a chocolate cupcake with buttercream frosting

Enter ES6 arrow functions

Since we use anonymous functions so frequently in JavaScript, the new fat arrow functions in the ECMAScript 2015 spec are most welcome. They afford the following advantages:

  • concise syntax
  • lexical binding of this
  • several syntactic sugar features

Let’s take a look at the first two in action:

1
2
3
4
5
6
7
8
9
10
...
this.announce = function() {
console.log('And now, the moment you\'ve been waiting for! Presenting...');
setTimeout(() => {
console.log('...a ' + this.type +
' cupcake with ' + this.frosting + ' frosting');
}, 1500);
};
...

And now, the moment you've been waiting for! Presenting... ...a chocolate cupcake with buttercream frosting

Here’s another quick refactor using an arrow function and showing off its implicit return in a concise function body (no curly braces):

1
2
3
4
5
6
7
this.on('eat', (function() {
return this.type + ' cupcakes are delicious';
}).bind(this));
// this ^^ becomes this:
this.on('eat', () => (`${this.type} cupcakes are delicious`));

Arrow functions make it easy to inline anonymous functions in all those places we need them, such as asynchronous callbacks, higher order functions and event handlers. this works just like most people would expect. And the best part is, we can still .bind, .call , apply and otherwise use the old ES5 non-arrow functions to our heart’s content in ES6. The specification lets you mix and match both styles, so use whichever suits the situation.

caveats

Some libraries like jQuery and Backbone have their own strategies for this, which use ES5’s interpretation. For example, many jQuery callbacks use $(this) to pinpoint the target object out of the DOM.

1
2
3
$('#cupcake').click(function(e) {
$(this).append('<h1>more HTML</h1>');
});

An arrow function would cause the selector above to fail, since the context isn’t what jQuery expects. Backbone, on the other hand, passes around this as an additional parameter throughout its event system. Arrow functions should work fine with the framework and probably make Backbone code more concise:

1
2
3
4
5
6
7
8
9
this.collection.on('add', function() {
this.render();
}, this);
// from this ^^ to this:
myCollection.on('add', () => {
this.render();
});

Note that arrow functions can be invoked through .call and .apply but we can not pass in this as an argument, only regular variables. Also, arrow functions do not have their own arguments object. Rest parameters still work though. For other quirks and features of arrow functions, see the MDN reference.

Conclusion

I don’t think I’ve ever heard someone say something nice about legacy JavaScript’s specification for this binding. I personally find the behavior counterintuitive but workable. ES5’s notion of the keyword has its own logic and we must understand it. But going forward, the ES6 arrow function gives us another tool, one that delivers a little logical and syntactic sugar for our event-driven, async code.