This article was also published on Medium.
Using Bluebird
In general, I think promises are an amazing concept. Not just because I had that one course in college about Haskell and monads, but because they simplify how you can reason about your code. Also, they help you handling those synchronous errors with great stack traces.
We settled on using the then recently released Q 1.0. We had been using earlier version of it in other projects already and were quite familiar with the API.
Now, I’d choose Bluebird. In addition to most of Q’s goodness, it offers some new features like catching specific error classes and generator support, but what really seals the deal are the even better stack traces and the amazing performance (more information).
Additionally, Bluebird seems to prefer some good practices, e.g. treating a promise like a class instance and using new Promise()
(API) instead of using Promise.defer()
.
Promisifying Modules
Most of our asynchronous methods are pretty straight-forward: They are promise chains. Some are short, some are rather complex, calling helper modules and doing several things in parallel. E.g., using Q we wrote code similar to:
Q.fapply(function getTeamById () {…})
.then(function getFileById (team) {…})
.then(function deleteVideoOnVimeo (team) {…})
.then(function removeFileFromTeam (team) {
return Q.ninvoke(team, 'save');
})
.spread(function success (team, rowsAffected) {…})
.fail(next);
(In this example, all functions in the promise chain were written inline– even though they have names for better stack traces. You could also declare all functions and then write the chain with just references. Even better, write abstract functions that use currying and can be used in several chains.)
But some third-party modules didn’t support promises (or at least, don’t support them well — like Mongoose). To keep our code consistent and always use promises, we often used Q’s ninvoke()
(Bluebird offers .promisify()
and there is also a module called ninvoke that clones Q’s API for Bluebird). Ideally, we would replace the save
method of the model with one that automatically returns a promise.
Another module we should have wrapped to return promises is SuperAgent, a really helpful module to write short and precise HTTP tests. In fact, we already made a subclass of it to support setting custom headers (for authorization using JWS), so overwriting the .end()
method to return a promise should be easy and possible remove some very ugly ninvoke()
calls.
If you think this all the way through, we should probably have also written a thin wrapper around restify’s route handler to make the controllers a bit leaner. Currently, those are functions with a signature of (request, response, next) -> void
, but most would work equally well as (request) -> Promise
, where the promise’s state determines whether the data should be send as response (with status code 200) or it should be propagated as an error. (This wouldn’t work for streaming responses, of course.)
Promises in Mocha Tests
Our API tests are written using Mocha, Chai and the aforementioned SuperAgent.
Most of our API tests look like this (in CoffeeScript):
describe "Post API", ->
before (done) -> …
describe "GET /posts", ->
it "should retrieve an array of posts", (done) ->
request(app)
.get(postsPath)
.expect(200)
.end (err, res) ->
return done(err) if err
expect(res.body).to.be.an('array')
expect(res.body.length).to.be.gt 0
done()
That’s an easy case: When you give Mocha’s it()
method a function that has one argument, it’s treated as a callback that determines when the test is done (with success or failure). But what about a test where we first create several posts, then log in an moderator and check that all are visible? Lot’s of Q.invoke(makePost(), 'end')
await us if we want to avoid callback-hell.
But, fear not: Since we started the project, Mocha 1.18 was released and with it a very nice feature: Promise support. That means, instead of calling a done()
callback, you can instead just return a promise. The promise’s final state will then determine if the test was successful or not.