Promises you can trust
JavaScript Promises provide a strong programming model for the future of JavaScript development.
So here I’m playing with promises.
First I need a bit of a package.json file:
{
"name": "promises",
"scripts": {
"test": "node node\_modules/mocha/bin/mocha"
},
"devDependencies": {
"chai": "^1.10.0",
"mocha": "^2.0.1"
},
"dependencies": {
"q": "^1.1.2"
}
}
Now I can write my first test (test/promises_test.js):
var Q = require('q');
var expect = require('chai').expect;
describe('promises', function() {
it('can be resolved', function(done) {
var promise = Q.Promise(function(resolve) {
resolve();
});
promise.then(function() {
done();
});
});
});
Notice that the “it” function takes a “done” function parameter to ensure that the test waits until the promise has been resolved. Remove the call to done() or to resolve() and the test will timeout.
it('can pass resolution value', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(6*8);
});
promise.then(function(value) {
expect(value).to.equal(42);
done();
});
});
This test fails, but because of a timeout. The reason is that done is never called. Let’s improve the test.
it('can pass resolution value', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(6*8);
});
promise.done(function(value) {
expect(value).to.equal(42);
done();
});
});
Using “done()” instead of “then()” indicates that the promise chain is complete. If we haven’t dealt with errors, done will throw an exception. The test no longer times out, but fails well:
promises
V can be resolved
1) can pass resolution value
1 passing (11ms)
1 failing
1) promises can pass resolution value:
Uncaught AssertionError: expected 48 to equal 42
+ expected - actual
+42
-48
at C:\\Users\\jhannes\\experiments\\promises\\test\\promises\_test.js:22:24
And we can fix it:
it('can pass resolution value', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(6*7);
});
promise.done(function(value) {
expect(value).to.equal(42);
done();
});
});
Lesson: Always end a promise chain with done().
In order to separate this, you can split the then and the done:
it('can pass resolution value', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(6*7);
});
promise.then(function(value) {
expect(value).to.equal(42);
}).done(done);
});
There is another shorthand for this in Mocha as well:
it('can return the promise to mocha', function() {
var promise = Q.Promise(function(resolve) {
resolve(6*7);
});
return promise.then(function(value) {
expect(value).to.equal(42);
});
});
But what is a promise chain?
it('can pass values in a promise chain', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(6);
});
promise.then(function(v) {
return v*7;
}).done(function(v) {
expect(v).to.equal(42);
}).done(done);
});
This is extra cool when we need multiple promises:
it('can resolve multiple promises', function(done) {
var promise = Q.Promise(function(resolve) {
resolve(['abc', 'def', 'gh']);
});
promise.then(function(array) {
return Q.all(array.map(function(v) {
return Q.Promise(function(resolve) {
resolve(v.length);
})
}));
}).done(function(lengthArray) {
expect(lengthArray).to.eql([3,3,2]);
}).done(done);
});
Notice that done is only called when ALL of the strings have had their length calculated (asynchronously).
This may seem weird at first, but is extremely helpful when dealing with object graphs:
var savePurchaseOrder = function(purchaseOrder) {
return orderDao
.save(purchaseOrder)
.then(function(orderId) {
purchaseOrder.orderLines.forEach(function(line) {
line.orderId = orderId;
});
return Q.all(
purchaseOrder.orderLines.map(orderLineDao.save));
});
};
Here, the save methods on dao.orderDao and orderLineDao both return promises. Our “savePurchaseOrder” function also returns a promise which is resolved when everything is saved. And everything happens asynchronously.
Okay, back to basics about promises.
it('can reject a promise', function(done) {
var promise = Q.Promise(function(resolve, reject) {
reject('something went wrong');
});
promise.then(null, function(error) {
expect(error).to.equal('something went wrong');
}).done(done);
});
Here, the second function to “done()” is called. We can use “fail()” as a shortcut:
it('can reject a promise', function(done) {
var promise = Q.Promise(function(resolve, reject) {
reject('something went wrong');
});
promise.fail(function(error) {
expect(error).to.equal('something went wrong');
done();
}); // trailing promise!
});
But this is not so good. If the comparison fail, this test will time out! This is better:
it('can reject a promise', function(done) {
var promise = Q.Promise(function(resolve, reject) {
reject('something went wrong');
});
promise.fail(function(error) {
expect(error).to.equal('something went wrong');
}).done(done);
});
Of course, we need unexpected events to be handled as well:
it('treats errors as rejections', function(done) {
var promise = Q.Promise(function(resolve, reject) {
throw new Error('whoops');
});
promise.fail(function(error) {
expect(error).to.eql(new Error('whoops'));
}).done(done);
});
And of course: Something may fail in the middle of a chain:
it('treats errors as rejections', function(done) {
var promise = Q.Promise(function(resolve, reject) {
resolve(null);
});
promise.then(function(value) {
return value.length;
}).fail(function(error) {
expect(error.message).to.eql("Cannot read property 'length' of null");
}).done(done);
});
The failure is automatically propagated to the first failure handler:
it('propagates failure to the first failure handler', function(done) {
var promise = Q.Promise(function(resolve, reject) {
resolve(null);
});
promise.then(function(value) {
return value.length; // Throws Error
}).then(function(length) {
this.test.error("never called");
}).then(function(number) {
this.test.error("never called");
}).fail(function(error) {
expect(error.message).to.eql("Cannot read property 'length' of null");
}).done(done);
});
It took me a while to become really comfortable with Promises, but when I did, it simplified my JavaScript code quite a bit.
You can find the whole source code here. Also, be sure to check out Scott Sauyet’s slides on Functional JavaScript for more on promises, curry and other tasty functional stuff.
Thanks to my ex-colleague and fellow Exilee Sanath for the inspiration to write this article.