One of the common critiques of node.js is that its (events-based) asynchronous model requires a lot of nested callback functions and generally uglier flow control than a synchronous model would allow. An example:
fs.readFile(path.join(base, 'config.json'), function(err, data){ if (data) { data = JSON.parse(data); data.forEach(function(path){ fs.readFile(path, function(err, data){ if (err) { log(err) } else { data = JSON.parse(data); if (data.url) { http.get(url.parse(data.url), function(){ // ...absurdum ad infinitum }); } } }) }) } }); |
This isn’t a trait unique to node.js though. Much of JavaScript, since it is a language heavily bound to the client-side and therefore DOM and user-initiated events, is very liable to “Rampant Callback Syndrome”.
Rampant Callback Syndrome
Rampant Callback Syndrome is a serious issue in the JavaScript arena and if left unchecked will continue to spread like a disease, poisoning our dreams with its ominous creep.
In all seriousness, it is rather annoying, and is definitely something that can be avoided with nothing more than a light dose of decent application design and thought.
One pattern that I’ve become fairly attached to is the simple premise of providing an API to begin with, and letting it appear to be a (mostly) synchronous interface, but behind the scenes throw around events to make it work properly.
Let’s say, for example, that we want to expose a Model API, for interfacing with our database.
When the model is instantiated we go about opening the connection to the database:
function Model() { // (not shown) ... configuration stuff ... this.server = new mongodb.Server( this.config.address, this.config.port, {} ); // (not shown) ... error checking ... this.db = new mongodb.Db(this.config.dbName, this.server, {}); this.db.open(function(error, client){ if (error) { this.error('DB: ' + this.config.db + ' error: ', error); return; } this.collection = new mongodb.Collection(client, this.config.dbCollection); this._isOpen = true; this.emit('open'); }.bind(this)); // (not shown) ... other stuff ... } |
The problem is that our new Model
call will return before the connection to the database is open. Fortunately, as you can see above, in the callback to the db.open
method we are setting a flag, _isOpen
, to true
, and we’re emitting the open
event. This Model happens to implement the events.EventEmitter
interface which means we can fire and trigger on our object.
So, let’s look at our calling-code:
var myModel = new Model({ /* config */ }); myModel.find({ username: 'James' }, function(data) { // I can live with a single nested function. }); |
This is great! The code above shows none of the callback nonsense we saw earlier[1] — we’ve abstracted it away. One big problem though is the find
method is going to be called before our database connection is open. But, thanks to the events interface, we can hold off until our open
event fires:
Model.prototype.find = function(selector, cb){ if (this._isOpen) { // If we're already open, let's go: this.collection.find(selector).toArray(cb); } else { // Otherwise, wait: var args = arguments; this.on('open', function(){ this.find.apply(this, args); // (added stability would be remove the event listener here) }); } }; |
In the else
block we register a new event listener, to listen for the open
event, and when it fires, we’re simply going to call the very same method (find
) with the very same arguments. Obviously this is only possible if we’re using callbacks (one of the arguments to find()
). Obliterating all callbacks is not we’re looking for; we’re simply looking to minimise.
Abstraction is the key
We can cure 90% of Rampant Callback Syndrome by simply abstracting our code.
The great thing about the solution shown in this post is that you’re able to provide the promised API immediately following instantiation — you’re not forcing the higher level of abstraction to fuss with what is a lower concern.
[1]: Yes, I know they’re doing completely different things. My point still stands.
Thanks for reading! Please share your thoughts with me on Twitter. Have a great day!
Alternatively you can just used named functions. This will solve 90% of your problems
@Raynos, Yep, true. That’s the gist of what I meant… i.e. abstraction.
A bunch of named functions in a single scope can get messy though. Further abstraction might be the key. E.g. a
ConfigFileReader
class.I often use the step library in node…
Read more here: https://github.com/creationix/step
What the Shell solves the problem of providing ‘Synchronus’ – look and feel to your, in fact, ‘aSynchronus’ code.
Step is definitely a solution but WTS provides something more than just Steping.
for ex:
** the code is typically bigger in WTS but the reusability is much higher.
WTS ‘Script’ comes to the rescue in real time applications where code/logic is much bigger than a couple of lines.
@Strx: Interestingly, you donโt need function names here, but they do improve the readability of your code.
@Raynos: This is smart because it allows for easier unit testing…
@Strx: I would think this patter would be tough to unit test…
This is some pretty nice information. I haven’t gotten into Node much yet, but I had been wondering about the nested callback problem. I didn’t really think too hard about it, but now I don’t have to. :p
Dustin Diaz made an MVC framework for Node.js called Matador (http://dustindiaz.com/matador) which I would assume takes a lot of this sort of stuff into account. I’m planning on going through it and giving it a test run and hopefully I can get a nice review and possibly some tutorials onto my blog (http://joezimjs.com).
yeah Ive found also that a lot of times the simple patterns are the best to keep things clean and functional. sweet thanks for the code ๐