Thursday, 9 July 2015

Callbacks suck

Problem: Javascript is essentially run on one thread and some things take time, blocking other requests.
Solution: make everything asynchronous and provide a callback mechanism.
Problem: I now have plenty of asynchronous calls which come back to a callback which does another async call and so on and so forth. Since I didn't really think this through to start with (!!), I now have something like:

This code is an unmaintainable mess and something has to be done about it.

There are a few ways to slay this beast:

#1 Refactor
You could refactor your code to pull out methods and reference those. Chances are you end up with something a little spaghetti-like: now you have all these neat little functions but actually determining the call order is an exercise in using your IDE's "go to definition" function, or at the very least, VIM's * and # commands. Not ideal, at least not from where I stand. Refactoring is a good process but I don't think it's the right tool to fix this.

#2 Node async module
You could alter your programming pattern and make use of the (rather cool) async node module. Off the top, it looks like it has some interesting solutions to some of the most common logic patterns, but I'm still not 100% sold. There's nothing intrinsically wrong with the module -- it just doesn't seem like the best fit (to me).

#3 Promises (to the rescue, again)
Promises have been around for a while and they're not going away. Indeed, ES6 brings promises right into the core language. If you aren't using them, it's at least time to start looking at them. And if you do, and you happen to stumble across one of the grandfathers of the promise library offering, Q, then you will be pleased to find that Q has support baked in to provide a promise-based wrapper around the typical NodeJS async callback: Q.nfbind, or, as I prefer, the alias: Q.denodeify.

You can just do something like:

var promiseBasedFunction = Q.denodeify(someAsyncFunction);
promiseBasedFunction('arg1.level0', 'arg2.level0').then(function(result) {
// first call succeeds, let's carry on
  return promiseBasedFunction('arg1.level1', 'arg2.level1');
}).then(function(result) {
// second call succeeds, let's forge on forward
  return promiseBasedFunction('arg1.level2', 'arg2.level2');
}).catch(function(error) {
// handle errors
  console.log('FAIL: ' + error);
}).done(function() {
// this is always run
  console.log('Exiting now...');

Some wins you get out of this:
  1. No deep nesting. There is a nice follow-on of functionality, something you can read top-down and which someone else could probably get into a little quicker. Code readability consistently ranks highly when programmers are asked to list aspects of quality code. Remember that the code you expertly write today will be visited by a complete stranger some time in the future. That stranger may even be you.
  2. No need to handle errors at every asynchronous turn. The first one that fails ends up in the catch area and you can deal with it there
  3. A defined end-point that is easy to deal with (the done handler)
But wait, there's more!
The async node module has some interesting techniques for dealing with simultaneous async calls (think: I need to get data from three web services and provide a consolidated answer -- I'd prefer to make those calls in parallel and consolidate once) -- but so does Q, with Q.all(). Q offers a lot, and one of the main benefits it offers is helping to wean you off of the callback tit. As I've shown in my promises tutorial, different promise libraries play together fairly well, so even if you decide to switch to another (or the ES6 built-in Promise), you have to freedom to do so.

Not that I suspect Q is going anywhere any time soon.

Footnote: this journey started when I had the brainfart that it would be great if there were a node module that provided functionality to wrap existing async calls in the promise lingo. I was about to embark on writing one when a friend suggested that I check out some of the functionality in Q that I wasn't already using. That's a reminder to check if a problem has already been solved before running off to write code -- though the process (like the process of writing my own promises implementation) would have probably been enlightening. It's also probably a reminder that it can be quite beneficial to skim over the full documentation for a library to discover useful gems.

No comments:

Post a Comment

C# Evolution (and why you should care) I may be a bit of a programming language nut . I find different languages interesting not just b...