Promises Broke My Return (and Coroutines Fixed It)
We all know writing asynchronous code is hard.
Even though async and other libraries do a great job at cooling down the infamous callback hell, most programmers would agree that promises make writing code in JavaScript way simpler. Especially since bluebird came along.
Now, I heartily concur that promises make one's life much easier. They offer consistency and composability (in other words we can map/reduce them and chain them). Which is good. Great, even. Right? It's a better way of dealing with asynchronicity. All the goodness of blocking calls without sacrificing the easier-to-reason-about single thread execution model. All hail promises. Right?
Well not quite.
Broken Return
You see, as we write code and dig deeper and our promise chains grow longer and become more complicated, we soon discover our return
statement is kind of, uh, broken.
Consider this code (disregarding error handling for simplicity):
function handleEvent () {
return getUser(user => {
if (!user) {
// Expected situation (for the sake of example).
return false;
}
return log.insertAsync({handledAt: moment()})
})
.then(user => {
return user.update({key: req.body.value})
})
.then(() => {
return true;
})
}
It needs an "asynchronous if". And it's broken, of course - because in the second promise handler, user will be false
so it'll crash with a TypeError
.
There are essentially two solutions:
- Nest the "conditional branches" accordingly.
- (Bluebird-specific) Reject the promise with a dummy error and use a catch predicate to handle it.
In other words either we create a complex nested chain - which is essentially callback hell in disguise - or we abuse errors for control flow. Ugh. Not great.
What we actually need is an explicit "exit the promise chain" statement. A meta-return
, so to speak. With promises, single return doesn't cut in anymore (as it wouldn't with vanilla callbacks either).
Enter Coroutines
Coroutine is a function marked with *
which can suspend itself and can resume at a later point in time, simply put. Coroutines are enabled by and built on top of a new ES6 feature called generators (which by the way other languages, e.g. Python, had for years).
The awesome thing about coroutines is that they allow us to write async code that looks synchronous while staying away from threads. And as a side effect, we get our desired meta-return
keyword, here called yield
.
With coroutines, our previous example could be rewritten as:
const {coroutine: co} = require('bluebird');
const handleEvent = co(function* () {
const user = yield getUser;
if (!user) return false;
yield log.insertAsync({handledAt: moment()});
yield user.update({key: req.body.value});
return true;
});
The code has three distinct advantages:
- Much simpler.
- Explicitly visible that
handleEvent
is a coroutine. - Different syntax for returning from the function and resuming execution (
yield
vsreturn
) => easier control flow.
With regards to this article, point #3 is the most interesting.
With coroutines we now have two control flow keywords: return
and yield
. return
works the way it would in a synchronous function (disregarding the fact that coroutine returns a promise) and yield
is its asynchronous counterpart.
The two combined give us a saner asynchronous conditional control flow. In other words: we can do a more reasonably looking "ifs" in our asynchronous code. Yay!
Disadvantages
Now, of course, it's not all roses and cherry pies.
One problem with coroutines as they are implemented now (i.e., on top of generators) is that instead of breaking return
, we're breaking yield
; in other words, we just made writing generators awkward. However, this will be fixed in ES7 which will have new async/await keywords. We'll just have to wait this one out.
Another minor issue is that generators are only natively supported in node 0.11+. And at this time, node 0.10 is still a safer bet. But hey, Facebook's regenerator - a tool that transpiles generators to vanilla ES5 code - is quite battle-hardened and so is babel so I wouldn't worry too much here.
Conclusion
Coroutines are a way to simplify our asynchronous code and allow more flexibilty with regards to the control flow ("asynchronous conditional branches") by introducing a new return-like keyword, yield
. With tools such as regenerator and babel we can start using them in ES5-only environments even today.
So welcome to the world of tomorrow!
Synchronously agree? Asynchronously disagree? I'm @tomas_brambora!