Taming 3rd party APIs with javascript promises


Javascript in the browser means interacting with humans and humans interact very slowly. Whether we're performing an animation, making an AJAX request, or waiting around for a user action, we can't ask the computer to sit idle. But that's just what happens if we don't release control of the "event loop". Our first line of defence is the callback, but if we're not careful things have a tendency to, run away on us...

a(function(resultFromA) {
  b(resultFromA, function(resultFromB) {
    c(resultFromB, function(resultFromC) {
      d(resultFromB, function(resultFromD) {
        e(resultFromB, function(resultFromE) {
          console.log(resultFromE);
        });
      });
    });
  });
});

Until iterators and generators make it to the browser, continuation-passing style is the only show in town, and CPS means callbacks. The good news is that nobody said we have to respond to callbacks right away.

Enter Promises

Promises capture the notion of an eventual value into a POJO. First a specification, then a construct, finally a first class citizen. Promises allow us to flatten a callback stack into a sequence of instructions.

a()
  .then(b)
  .then(c)
  .then(d)
  .then(e)
  .then(console.log);

There are plenty of great introductions to promises that can help you incorporate promises into your own code, but here I'm interested in one particular trick.

Taming third party APIs

Node.js programmers are intimately familiar with error first callbacks.

require('fs').readFile('a.txt', (err, data) => {
  if (err) {
    return console.error(err);
  }
  console.log(data);
});

These are functions that accept any number of arguments, but the last one is always a callback. Specifically a callback with two parameters, the first one for any potential errors and the second for our data if everything works out. Node programmers also have a way of dealing with this particular style of function.

Promisification

The process of wrapping an error-first-callback function in something a little more user-friendly. Something which returns a promise, but still does the messy work of providing the necessary callback and checking for an error in the response.

Flat is better than nested.

- The Zen of Python

In true NPM fashion there are a number of micro-libraries that can provide this for you, like js-promisify, or promisify-node, or as a helper in your favourite promises library Promise.promisify. Or, you can write your own with a few lines of JS (and a Promise implementation).

With a promisify we can get the same sensible control flow as if we had written the original api ourselves.

var readFile = Promise.promisify(require("fs").readFile);

readFile('a.txt')
  .then(console.log)
  .catch(console.error);

But enough contrived examples, let's see what promisify can do with a real ball of yarn. Recently I've been working with the new Braintree JS v3 SDK which although very powerful, and I think very well designed, can easily lead to the callback hell we talked about in the beginning.

Wrapping 3rd party APIs

Of particular importance to me is the "Hosted Fields" API, for accepting credit card information without having to worry about all the fuss and muss of PCI compliance. Braintree helpfully provides a number of examples, one of which I have modified slightly and included here.

Using callbacks

See the Pen Hosted Fields: demo by Sinetheta (@Sinetheta) on CodePen.

There's plenty going on here, but for the purposes of this discussion I'm interested in two things. First, we can see the indentation that comes with nested callbacks, and second it's not immediately clear in what order everything's happening.

Now, we could fight this by creating more named functions and trying to split things up further. But, since most of these steps must happen sequentially and require references from their predecessors, we're in prime Promisify territory.

Using promises

See the Pen Hosted Fields: With promises by Sinetheta (@Sinetheta) on CodePen.

The differences aren't earth shattering, but it's not like we had to work very hard at it either. By wrapping a few 3rd party functions in promises we were able to clarify our startup process.

Promise.promisify(braintree.client.create)({
  authorization: authorization
}).then(createHostedFields)
  .then(bindForm)
  .catch(console.error);

Best of all we were able to generalize our error handling. I find that all too often error modes are an after thought and that un-handled exceptions become the norm. At least with a single promise chain I'm able to funnel messaging to a reporting tool like Bugsnag or better yet, provide actionable feedback to a user if we're talking about the control of on-page interactions.

Gotcha

There is one other thing worth noting before you start wrapping someone else's code in promises, and that's execution context. Towards the bottom of my example there is a curious line.

Promise.promisify(hostedFieldsInstance.tokenize, {
  context: hostedFieldsInstance
})()

This extra argument is necessary because it turns out that tokenize is context sensitive. When we use an API in a way it's author's didn't anticipate we can sometimes run afoul of their assumptions. In this case, we've extracted a "class method" for our own purposes and lost the reference it needed.

In browsers, console.log used to work the same way. Until recently, invoking it with any other context than console (for example, by using .call or .apply) would lead to a TypeError: Illegal invocation.

We always have the option of using Function.prototype.bind to circumvent the issue, but most implementations of promisify will give you a more straightforward path, as Bluebird has done for us here.