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.
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, Z̪̝̞̙̞̾a͕̼̹̣̬ͧ̔ͧ͆̌l̜̳͓͒g̦̔͂ͬͫ̓͊͗ȍ̫͊̉.
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.
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.
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.
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.
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.
What?
Whaaaaat?