UPDATE 14/06/15: Realised the existence of Promise.resolve
, added info on Promise.all
.
There is so much I love about the functionality and syntax coming through under the banner of ES6. One such piece of functionality, is the 'Promise'. Promises are not something that needs to be transpiled, as of writing, all but IE and Opera Mini have support out of the box. The stragglers can be polyfilled quite easily.
What follows, are three tips for using promises more effectively.
Chain, don't nest
When I first started playing with promises, I found myself nesting code blocks more than I would have liked. Code like:
class Example {
saveData(data) {
return new Promise(function(resolve, reject) {
// Save Data
resolve(data);
});
}
getFromWeb(id) {
return new Promise(function(resolve, reject) {
// Get from web
resolve(data);
});
}
display(id) {
let self = this;
return new Promise(function(resolve, reject) {
self.getFromWeb(id)
.then(function(data) {
self.saveData(data)
.then(function(data) {
// Display somewhere
resolve();
});
});
});
}
}
new Example().display(1);
Not very readable and not making great use of screen real estate, when you can actually do:
display(id) {
let self = this;
return self.getFromWeb(id)
.then(function(data) {
return self.saveData(data);
})
.then(function(data) {
// Display somewhere
return data;
});
}
The display
function is doing exactly the same, but now the functionality is chained. The second then
function deals with the display logic, before returning the data param, enabling the display
function to be chained itself:
new Example().display(1).then((data) => { /* Work on data */ console.log('async finished'); });
Empty Promises
I'm one of those people who has never read a VCR manual. I pick up and do, realising only years later, that I didn't need to rush home every time I wanted to record something, because the VCR had a timer. I once wrote a really handy little function in SQL called VALUENULL
, for dealing with NULL values. I can't believe that sort of functionality wasn't built in, oh wait, ISNULL.
Well, I find myself in that place again. After triumphing that I'd come up with such a simple way to provide consistent Promise
returning functions with Util.emptyPromise
(see below), then worring that such a thing might be considered bad practice.
class Util {
static emptyPromise(val = null) {
return new Promise((resolve) => { resolve(val); });
}
}
You see, the point of the function is to wrap a value (or no value) around a prefab Promise
that always resolves. You would do this if you were creating a non-blocking/asynchronous API on top of synchronous code. Or if you envisaged blocking code becoming asynchronous in the future and wanted to ensure that the public API didn't feel the affect of such massive breaking changes.
A prime example of this, is when I recently wrote a data layer based on localstorage
(which is synchronous), then decided that localstorage
wasn't cutting the mustard, so replaced with localForage (which is Promise
based). That weekend is one I won't forget in a hurry.
My point is, Util.emptyPromise
is a less elegant equivalent to the already existing Promise.resolve. I'll leave this section with the original pun, because it still makes me chuckle.
The function is poorly named, because it can return a value. I just like the pun. An example of the pun in action:
class Election {
fullOf() {
return Util.emptyPromise()
.then(() => { return Util.emptyPromise(); });
}
}
Promise.all
You may want to check up my sleeves at this point, because I'm about to make bunnies appear out of thin air.
'Callbacks' is just something you do if you're writing non-blocking JavaScript. Calllbacks, within callbacks, within callbacks. Callbacks are there so that you can control the flow of some logic, which has a dependancy on asynchronous code (like an Ajax request), that will take you away from the main 'blocking' execution thread.
Promises take these callbacks and makes them look a lot prettier, while also providing a platform for deferring the attachment of callbacks. The following example still fires the console.log
, even though the callback is attached after the Promise
has already resolved.
var p = Promise.resolve();
p.then(function() { console.log('test'); });
But there is still room to make our code damn right gorgeous. Consider the following code:
function asyncFuncA() {
return new Promise(function(r) {
setTimeout(() => { r('asyncA'); }, 2000);
});
}
function asyncFuncB() {
return new Promise(function(r) {
setTimeout(() => { r('asyncB'); }, 1000);
});
}
class AsyncController {
render(template, data) {
return new Promise(function(resolve, reject) {
// Do render stuff
resolve({ t: template, d: data });
});
}
asyncAction(route) {
return asyncFuncA()
.then(function(a) {
return asyncFuncB()
.then(b => { return [ a, b ]; });
})
.then(data => { return this.render('route', data); });
}
}
let c = new AsyncController();
c.asyncAction()
.then((obj) => { console.log(`${obj.d[0]} + ${obj.d[1]}`); });
Looking at the asyncAction
. asyncFuncA
and asyncFuncB
are chained by calling asyncFuncB
within the callback of asyncFuncA
. The call to the render
function starts on a separate tree, consuming the response of both asynchronous functions. A rocky sort of waterfall.
asyncAction
--> asyncFuncA
--> asyncFuncB
--> render
We can achieve the same with the function below. The second asynchronous function no longer has a dependancy on the first, and we only have to call then
once.
asyncAction(route) {
return Promise.all([ asyncFuncA(), asyncFuncB() ])
.then(data => { return this.render('route', data); });
}
asyncAction
--> asyncFuncA
--> asyncFuncB
--> render
Pretty hot!