Today I’m here to preach the gospel of promise-based async programming in Node.js, when coupled with an exciting new Harmony feature called generators.

If you’ve been writing Node.js code for a while, then I’m sure you’re familiar with the Node.js callback model. You’ve probably also heard some of the more common nicknames for it: callback hell, callback soup, pyramid of doom, etc. It looks a little something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var checkIfBaconIsDelicious = function(cb) {
    // Truth may have been cached in our NoSQL key/value of choice.
    redis.get("bacon:delicious", function(err, cachedValue) {
        // Handle errors. Every. Time.
        if(err) return cb(err);
        if(cachedValue) {
            return cb(null, cachedValue);
        }
        // Okay it's not cached. First consult with the bacon oracle.
        baconOracle.consult(function(err, oracleWisdom) {
            // Don't forget them errors!
            if(err) return cb(err);
            if(oracleWisdom !== null) {
                return cb(null, oracleWisdom);
            }
            // Oracle must not be home today. Let's do some scientific
            // bacon analysis.
            baconAnalyser.run(function(err, analysisResult) {
                // (You can hear the screams of the 80char print margin purists at this point)
                if(err) return cb(err);
                if(analysisResult.conclusive) {
                    return cb(null, analysisResult.conclusion);
                }
                // Okay, bacon analysis wasn't conclusive. Let's get a
                // representative sample of the public and reach a verdict.
                democraticBaconConsensus.sample(function(err, thePeople) {
                    if(err) return cb(err);
                    cb(null, thePeople.verdict);
                });
            });
        }
    });
};

The example, while a little contrived, hopefully demonstrates the kind of crazy stuff that can (and does) happen in a Node.js codebase. I’m here to tell you that there’s light at the end of the tunnel. Ladies and gentlemen, I give you … Generators!

Buh… promises?

Yes, Promises!

Buh… generators?

Generators, also known as (semi-)coroutines, are methods that …

… allow multiple entry points for suspending and resuming execution at certain locations. …
Wikipedia

They’re a very powerful construct for advanced forms of iteration. MDN has a great intro write-up on generators that I’ll refer you to if you haven’t dealt with generators much before. Note however that the MDN article covers generators from the ill-fated Javascript 1.7 era, so the syntax in the examples differs (slightly) from the Harmony-style generators I’ll be using in this article. Make sure you have a read if you don’t already understand generators, as we’ll be diving into some heavy stuff very soon!

The promised land

Let’s look at the contrived example of pyramid-code I began this article with again, only this time let’s try using promises. There’s a few great promise implementations out there, but for the rest of this article, assume we’re using Q.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Pretend that we have promisified our redis / baconOracle / etc libraries
// using Q.nbind, so that instead of expecting a callback parameter they
// return promises.
var checkIfBaconIsDelicious = function(cb) {
    return redis.get("bacon:delicious").then(function(cachedValue) {
        if(cachedValue) {
            return cachedValue;
        }
        return baconOracle.consult().then(function(oracleWisdom) {
            if(oracleWisdom !== null) {
                return oracleWisdom;
            }
            return baconAnalyser.run().then(function(analysisResult) {
                if(analysisResult.conclusive) {
                    return cb(null, analysisResult.conclusion);
                }
                return democraticBaconConsensus.sample().then(function(thePeople) {
                    cb(null, thePeople.verdict);
                });
            });
        });
    })
    .fail(function(err) {
        cb(err);
    });
};

But hold on just one gosh-durned second! We still have pyramids! Sure the code is a little cleaner – we don’t have to handle errors everywhere – but we could have done that with the domain module! Furthermore, we still have all this explicit promise management code. While (arguably) a little less noisy than standard node callbacks, we’re still littering then() calls all over the place. Ugh.

Often times, promises do help avoid excess indentation for async code, but not always. For example, when you have a lot of sequential steps that can have early bailouts, you can still create the callback ladders that you know and love so much.

If you’re gearing up to pelt me with Nerf bullets because you’re convinced I brought you all this way just to let you down, read on just a bit further. This is the part where the plan comes together and you achieve enlightenment.

The part where you achieve enlightenment

Ready?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var checkIfBaconIsDelicious = function(cb) {
    // Generator time!
    Q.spawn(function *() {
        // I'm yielding ... promises?
        var cachedValue = yield redis.get("bacon:delicious");
        // Wait a second...
        if(cachedValue) {
            return cb(null, cachedValue);
        }
        // There's no callbacks...?
        var oracleWisdom = yield baconOracle.consult();
        if(oracleWisdom !== null) {
            return cb(null, oracleWisdom);
        }
        // Awwww shiiiiiii--
        var analysisResult = yield baconAnalyser.run();
        if(analysisResult.conclusive) {
            return cb(null, analysisResult.conclusion);
        }
        // This is actually happening.
        var thePeople = yield democraticBaconConsensus.sample();
        cb(null, thePeople.verdict);
    });
};

The above snippet is doing the exact same thing I’ve shown you twice now with both with Node callbacks and promises. Exactly the same – except it’s not horrible to look at! It reads nicely, there’s no async management boilerplate, and the indentation is kept at a constant (and sane) level! This code is still executing asynchronously, but reads almost exactly the same as a synchronous method would. Were it not for the innocuous yield calls, it would read exactly the same as a synchronous counterpart. Code like the previous example allows you to focus on what is actually going on, as there’s a lot less noise coming from the async management code.

But wait, what actually just happened?

Q.spawn happened. What this method does is execute the generator method provided, and every time the generator method yields a Promise, Q will wait for that promise to be fulfilled. When it is, Q.spawn will call next() on the generator again, passing in the result of the fulfilled promise so that the generator method receives it as the result of the yield expression. Q.spawn will keep awaiting every promise yielded to it faithfully, until the generator method completes.

Thus you can write async code that reads like synchronous code, simply by yielding any asynchronous calls from your generator. Let’s look at how we might actually implement something like Q.spawn ourselves, since it’s actually quite simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Q = require("q");
var ourSpawn = function(generatorFn) {
    var gen = generatorFn();
    // This method will get called over and over to keep pumping fulfilled
    // promise values back into the generator.
    var pumpGenerator = function(val) {
        var ret = gen.next(val);
        if(ret.done) {
            // Generator has completed. We're done here.
            return;
        }
        // Calling Q like this just wraps whatever the generator gave us back
        // in a promise, if it wasn't already a promise.
        var result = Q(ret.value);
        result.then(function(val) {
            // The promise that was yielded to us has completed. Pass it back
            // into the generator and get ready for another round of fun.
            pumpGenerator(val);
        });
    };
    pumpGenerator();
};

That just about sums up what Q.spawn is doing under the covers. As you can see, there’s not really any magic, it’s just harnessing the awesomeness of generators.

If you’ve been paying attention though, you may have noticed I entirely omitted error handling from the previous example. Don’t worry, I didn’t do this on purpose to obscure some major caveat to glorious generators; I simply saved the best for last.

Error handling FTW

The last example needs error handling. Here it is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var checkIfBaconIsDelicious = function(cb) {
    Q.spawn(function *() {
        try {
            var cachedValue = yield redis.get("bacon:delicious");
            if(cachedValue) {
                return cb(null, cachedValue);
            }
            var oracleWisdom = yield baconOracle.consult();
            if(oracleWisdom !== null) {
                return cb(null, oracleWisdom);
            }
            var analysisResult = yield baconAnalyser.run();
            if(analysisResult.conclusive) {
                return cb(null, analysisResult.conclusion);
            }
            var thePeople = yield democraticBaconConsensus.sample();
            cb(null, thePeople.verdict);
        }
        catch(err) {
            cb(err);
        }
    });
};

You read that correctly: We’re handling errors in an asynchronous block of code, using standard try/catch. Generators, when suspended, allow you to continue execution with gen.next, but they also allow you to cause an error to be thrown at the suspended location inside the generator, using gen.throw. While our naive little implementation of Q.spawn didn’t demonstrate this, the real version will also handle rejected promises, and cause the errors from these promises to be thrown inside the generator that yielded the promise in the first place.

But I don’t like promises!

Fair enough. I didn’t really prefer promises over standard callbacks for quite a long time either. With excellent libraries like async, I don’t really feel there’s a clear winner in handling async code in Node.js when discussing promises versus the Node.js standard callback model. If you don’t feel like drinking the promise Kool-Aid just yet, then there are also libraries such as galaxy and suspend that build on Harmony generators to solve callback hell.

Here are the main reasons why I prefer using promises with generators in Node:

  • It’s less verbose than solutions like suspend library. You may have to do a little upfront boilerplate elsewhere to “promisify” external libraries first though. If these libraries are being used enough in your codebase, then the cost of wrapping them is extremely small.
  • No magic involved. Promises have a very simple contract, and as we saw earlier the code behind Q.spawn is very straightforward.
  • I actually like promises. I am a huge fan of caolan’s fantastic async library, and I’ve used it in several Node projects. However I’ve been using Q for a while now also, and find it easier to wrap my head around when dealing with more complex async flow control (Q.all ftw)

The bad news

The bad news is that generators are a relatively new feature of Harmony, and won’t have mainstream browser support for quite some time (IE10 doesn’t support them). On the Node.js side of things, they are presently available in Node 0.11 (when using the –harmony flag), but this is an unstable release that is probably not suitable for any kind of production use. Node 0.12 shouldn’t be too far away though, as 0.11 has been in development for a while now. So the good news is generators will be available in a stable Node.js release near you very soon!

In summary

Generators give us an exciting new way to write async code that is more concise, and above all, more readable. It will be available in the stable Node 0.12 series upon its imminent release, and will be available in browsers by the time your children’s children have learned how to sling Javascript. I’m excited for their application in Node async code however, and I hope you are too!