How To Extend A JavaScript API Based On Promises/A+

I was writing a little unsigned “dump my tabs”  WebExtension for my Firefox Developer Edition when I realized that, outside the jQuery world, I’d only ever used old-style callbacks before.

“No biggie”, I thought. It shouldn’t be too hard to google up the basic DOs and DON’Ts of writing your own “links in the then chain”. Well, it turns out that, no matter how hard I searched, it seemed like everyone was either trying to convince me to use them (already done), or explaining how jQuery does them (I’m not using jQuery), or teaching me how to use ready-made promise APIs, or ignoring caveats I specifically needed to know about.

An introduction to the underlying design pattern behind all of these Promises/A+-compliant APIs was conspicuously absent so, after stumbling around the web for an hour or two and having to resort to IRC channels to get answers to some of my problems, I decided this really needed to be written down. I’m still not sure I feel confident, but that’s more a symptom of being a perfectionist coding in JavaScript than anything else.

I’ll start with a simple synchronous example:

function mongleData(input_data) {
    return new Promise(function(resolve, reject) {
        try {          
            // Do stuff to produce output_data from input_data
            resolve(output_data);
        } catch (e) {
            reject(e);
        }
    }
 }

Now I’ll explain:

  1. We use a closure to match the “take data, return a Promise” pattern that other APIs use.
  2. We do everything within the body of the promise’s callback. (Most visibly, so we have access to the resolve and reject functions at all times.)
  3. We wrap everything in a try/catch block so we can redirect all errors into reject. (Yes, resolve(foo(x)) will reject any errors thrown by foo, but what about errors thrown while preparing x? Nearly every article I found just ignored that point.

With this basic API, we can now do things like browser.tabs.query({}).then(mongleData).then(somethingElse)

…so what about a promise around an asynchronous API? Well, let’s implement somethingElse:

function somethingElse(input_data) {
    return new Promise(function(resolve, reject) {
        try {
            let success = function(data) { resolve(data); }
            let failure = function(error) { reject(error); }

            doSomethingAsync(input_data, success, failure);
        } catch (e) {
            reject(e);
        }
    }
 }

Again, simple once you get the hang of it. The key is in understanding that resolve and reject basically mean “call the next step in the success/failure chain with this argument”.

The try/catch block isn’t strictly necessary, but it’s a good idea to get in the habit so that, if you do have something that can fail, the failure will get propagated into the promise system.

IMPORTANT: If you do further processing inside the success or failure callbacks, don’t forget to add more try/catch.

Finally, what if you want an elegant way to perform an asynchronous operation on each item in an array? For that, we use Promise.all:

function asyncPromiseForArray(data) {
    return Promise.all(data.map(asyncPromiseForItem))
}

promiseProducingAnArray.then(asyncPromiseForArray)

It’s just that simple. Here’s how it works:

  1. then() calls asyncPromiseForArray(data)
  2. asyncPromiseForArray then…
    1. uses Array.prototype.map to call asyncPromiseForItem on each entry in the data Array.
    2. Feeds the resulting array of promises to Promise.all to produce one promise which waits on all of the entries.

Suppose you want to write a promise which takes a list of URLs and retrieves their contents using XMLHttpRequest. Just write an asyncPromiseForItem that does it for one URL (See my previous “asynchronous promise” example) and let this code extend it to a list of URLs.

IMPORTANT: Promise.all fails if any of the promises in the list fail, so, in some circumstances, you may want to intentionally write promises which ignore certain errors (calling resolve rather than reject).

So, where to next? Well, I’d recommend reading these too:

CC BY-SA 4.0 How To Extend A JavaScript API Based On Promises/A+ by Stephan Sokolow is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

This entry was posted in Geek Stuff. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting a comment here you grant this site a perpetual license to reproduce your words and name/web site in attribution under the same terms as the associated post.       Also, please be aware that non-constructive comments will have their URL field erased before being approved in order to combat SEO spam.