Life in the Promise Land

I’ve been spending a lot of time these days in Node.js working with Promises. Our team has adopted them as our pattern for writing clean & comprehensible asynchronous code. Our library of choice is bluebird, which is — as it claims — a highly performant implementation of the Promises/A+ standard.

There’s plenty of debate in the JavaScript community as to the value of Promises. Continuation Passing Style is the de-facto standard, but it leads to strange coding artifacts — indentation pyramids, named Function chaining, etc. This can lead to some rather unfortunate and hard-to-read code. Producing grokable code is critical to honoring team-members and long-term module maintainability.

Personally, I find code crafted in Promise style to be much more legible and easy to follow than the callback-based equivalent. Those of us who’ve drank the kool-aid can become kinda passionate about it.

I’ll be presenting some flow patterns below which I keep coming back to — for better or for worse — to illustrate the kind of well-intentioned wackiness you may may encounter in Promise-influence projects. I would add that, to date, these patterns have survived the PR review scrutiny of my peers on multiple occasions. Also, they are heavily influenced by the rich feature-set of the bluebird library.

Performance Anxiety

Yes, Promises do introduce a performance hit into your codebase. I don’t have concrete metrics here to quote to you, but our team has decided that it is an acceptable loss considering the benefits that Promises offer. A few of the more esoteric benefits are:

  • Old-school arguments.length-based optional-arg and vararg call signatures become easier to implement cleanly without the trailing callback Function.
  • return suddenly starts meaning something again in asynchronous Functions, rather than merely providing control flow.
  • Your code stops releasing Zalgo all the time — provided that your framework of choice is built with restraining Zalgo in mind (as bluebird is). Also, Zalgo.

How Many LOCs Do You Want?

Which of the following styles do you find more legible? I’ve gotten into the habit of writing the latter, even though it inflates your line count and visual whitespace. Frankly, I like whitespace.

/**
 * fewer lines, sure ...
 */
function _sameLineStyle() {
    return example.firstMethod().then(function() {
        return example.secondMethod();
    }).then(function() {
        return example.thirdMethod();
    });
}

/**
 * yet i prefer this
 *   what a subtle change a little \n can make
 */
function _newLineStyle() {
    return example.firstMethod()
    .then(function() {
        return example.secondMethod();
    })
    .then(function() {
        return example.thirdMethod();
    });
}

It even makes the typical first-line indentation quirkiness read pretty well. Especially with a large argument count.

Throw Or Reject

I’ve found it’s good rule to stick with either a throw or Promise.reject coding style, rather than hopping back & forth between them.

/**
 * be careful to return a Promise from every exiting branch
 */
function mayReject(may) {
    if (may || (may === undefined)) {
        return Promise.reject(new Error('I REJECT'));
    }
    return Promise.resolve(true);
}

/**
 * or add a candy-coated Promise shell,
 *   and write it more like you're used to
 */
var mayThrow = Promise.method(function(may) {
    if (may || (may === undefined)) {
        throw new Error('I REJECT');
    }
    return true;
});

Of course, rules are are meant to be broken; don’t compromise your code’s legibility just to accomodate a particular style.

If you’re going to be taking the throw route, it’s best to only do so once you’re wrapped within a Promise. Otherwise, your caller might get a nasty surprise — as they would if your Function suddenly returned null or something equally non-Promise-y.

Ensuring that Errors stay inside the Promise chain allows for much more graceful handling and fewer process.on('uncaughtException', ...);s. This is another place where the overhead of constructing a Promise wrapper to gain code stability is well worth the performance hit.

Deferred vs. new Promise

The bluebird library provides a very simple convenience method to create a ‘deferred’ Promise utility Object.

function fooAndBarInParallel() {
    // boiler-plate.  meh
    var constructed = new Promise(function(resolve, reject) {
        emitter.once('foo', function(food) {
            resolve(food);
        });
    });

    // such clean.  so code
    var deferred = Promise.deferred();
    emitter.once('bar', function(barred) {
        deferred.resolve(barred);
    });

    return Promise.all([
        constructed,
        deferred.promise,
    ]);
}

I find code that uses a ‘deferred’ Object to read better than code that uses the Promise constructor equivalent. I feel the same way when writing un-contrived code which actually needs to respect Error conditions.

Early Return

Finally … here’s a lovely little pattern — or anti-pattern? — made possible by Object identity comparison.

// const ...
var EARLY_RETURN;

function _getSomething(key, data) {
    var scopedResult;

    return cache.fetchSomething(key)
    .then(function(_resolved) {
        scopedResult = _resolved;
        if (scopedResult) {
            // no need to create what we can fetch
            throw EARLY_RETURN;
        }

        return _createSomething(data);
    })
    // ...
    .then(function(_resolved) {
        scopedResult = _resolved;

        return cache.putSomething(key, _resolved);
    })
    .catch(function(err) {
        if (err !== EARLY_RETURN) {
            // oh, that's an Error fo real
            throw err;
        }
    })
    .then(function() {
        return scopedResult;
    });
}

The objective is to short-circuit the Promise execution chain. Early return methods often go hand-in-hand with carrying something forward from earlier in the Promise chain, hence the scopedResult.

There’s obtuseness sneaking in here, even with a simplified example using informative variable names. Admittedly, an early return Function is easier to write as pure CPS, or using an async support library. It’s also possible to omit the EARLY_RETURN by using Promise sub-chains, but you can end up with indentation hell all over again.

Clearly, YMMV.

I’d Say That’s Plenty

No more. I promise.