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:
- We use a closure to match the “take data, return a
Promise
” pattern that other APIs use. - We do everything within the body of the promise’s callback. (Most visibly, so we have access to the
resolve
andreject
functions at all times.) - We wrap everything in a
try
/catch
block so we can redirect all errors intoreject
. (Yes,resolve(foo(x))
willreject
any errors thrown byfoo
, but what about errors thrown while preparingx
? 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:
then()
callsasyncPromiseForArray(data)
asyncPromiseForArray
then…- uses
Array.prototype.map
to callasyncPromiseForItem
on each entry in thedata
Array. - Feeds the resulting array of promises to
Promise.all
to produce one promise which waits on all of the entries.
- uses
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:
- StackOverflow: ES6 promise settled callback? (How to implement
.finally()
) - A discussion of the quirks of promises on Hacker News
- Are JavaScript Promises swallowing your errors?
- StackOverflow: How to debug javascript promises?
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.
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.
All comments are moderated. If your comment is generic enough to apply to any post, it will be assumed to be spam. Borderline comments will have their URL field erased before being approved.