I’ll try my best to put together a coherent article on the subject that doesn’t put you to sleep. But I wouldn’t bet my horse on that.
I’d bet yours, though.
I do realize I got one month too late to the party, but like I mentioned, I was too busy visiting coffee shops in Amsterdam. Without further ado…
Case Study: Callbacks vs. Promises
Firstly, I would like to cite the series of resources that prompted this blog post.
- You’re Missing the Point of Promises, an introduction to promises
- Node’s biggest missed opportunity, the article that triggered the blog-post hell, by James Coglan
- Broken Promises, narrating why promises were
.reject()
'd in Node, by Mikeal Rogers - Broken Promises, another reply, depicting the pitfalls of promises and evangelizing patterns
- Callbacks, promises, and simplicity, where James makes some concessions and concludes his point
These are possibly the lengthiest articles written on the subject, but they definitely are the most interesting ones. I recommend to take your time and read through all of them, you won’t regret it.
[…]
So, you’ve read them all? Great!
Lets back up a little bit, now, and point out the source of the conflict. Node supported promises early on, but they decided to drop support for them shortly afterwards, partially because of the volume of disagreement they generated. Thus, they sticked with callbacks.
The different modules packaged through npm, swiftly followed the convention of passing function(err, results){}
as the last parameter, a convention was silently born.
My Point of View
While developing this blogging engine, and particularly the asset manager behind it, I made extensive use of async, which vastly improves upon the raw power of manually chaining callbacks.
I felt comfortable dealing with callbacks, but some more complex scenarios demanded a more robust solution, such as the ones async provides.
As far as promises go, I’ve been working with jQuery.Deferred for a while now, mostly in AJAX scenarios, though.
Recently, I started working on an application that makes more extensive use of promises, both through AngularJS on the client-side, and using Q on the server-side.
On the client-side, this makes perfect sense. We have jQuery performing similar operations, and the complexity requirements for asynchronous code aren’t as steep as they are in the back-end.
Front-end async operations tend to be way simpler than those performed on the server-side. There rarely is the need to create a deeply nested hierarchy of callbacks: an event handler making an AJAX request and maybe even doing something else with the response. You might even have a thin layer that creates a cache of the responses, but that’s about it. From this standpoint, it makes sense using promises.
On the other side, promises tend to result in more convoluted, complex code in Node.
To me, it’s not a matter of simplicity, as James puts it, but rather, a matter of both practicality and productivity.
Practicality, because it’s just more straightforward to use callbacks in an environment where it’s the standard to do so. Productivity is a side-effect bonus here, bending the application to use promises in a portion of the code would be an unwarranted waste of time.
And think of the drawbacks. It would also be unnatural and lead to a chaotic codebase where everyone has an opinion on how code should look like. It’s not even that promises are wrong, it’s just that, at this point in time, they don’t belong in Node.
Again, not because promises are wrong, or “broken”, but they don’t fit in, they don’t accomplish anything that can’t be done using callbacks. Besides, mixing both styles might even complicate matters, making it particularly confusing to handle errors, for example.
Error Handling Conventions in JavaScript
There is a very simple convention in JavaScript you should abide by, if you want to build successful and clean applications. And that is, how to write properly error-handling functions.
Synchronous code should
throw
, Asynchronous code shouldn’t. Ever.
function sync(){
try{
return taskThatMayThrow();
}catch(e){
console.log(e); // handle
}
}
function async(done){
operation(function(err, result){
if(err){
return done(err); // bubble up
}
result += 1; // [...] further processing
done(null, result);
});
}
Simple enough, when dealing with asynchronous code, you should always bubble exceptions up, so that the caller can choose to either handle them, or bubble them further up the callback chain. This way, your application can handle errors gracefully, rather than quit unexpectedly (or require process.on('uncaughtException')
in order to survive).
Conclusion
I definitely think Node is better off without promises, broken or otherwise. The client-side ecosystem is a little less well-defined, and as such, a more suitable home for promises, but even then, it’s just a matter of preference to use one or the other.
Comments