Node.js control flow using EventEmitter
Recently, I read yet another blog post complaining about how concurrency is hard in Node.js. As is often the case, the examples highlighting the problems are leaving a lot to be desired.
Since I’ve written quite a bit of Node.js, I’d like to present an example of concurrency style in Node.js that isn’t a “strawman” often touted as the bane of Node.js’ existence.
EventEmitter
If you’ve been around Node.js for a little bit, you might have heard something along the lines “everything is a stream”, or “everything is an event emitter.” There is a reason for that.
Let’s take the example from the blog post I mentioned:
function dostuff(callback) { | |
task1(function(x) { | |
task2(x, function(y) { | |
task3(y, function(z) { | |
if (z < 0) { | |
callback(0); | |
} else { | |
callback(z); | |
}); | |
}); | |
}); | |
} |
If you find yourself doing the above, you’re either in a rush, or should stop and rethink your design. Here’s how to accomplish a similar thing using continuation passing:
var dostuffEmitter = new EventEmitter(); | |
dostuffEmitter.on('task1', function (callback) { | |
dostuffEmitter.emit('task2', x, callback); | |
}); | |
dostuffEmitter.on('task2', function (x, callback) { | |
dostuffEmitter.emit('task3', y, callback); | |
}); | |
dostuffEmitter.on('task3', function (y, callback) { | |
dostuffEmitter.emit('finish', z, callback); | |
}); | |
dostuffEmitter.on('finish', function (z, callback) { | |
if (z < 0) { | |
callback(0); | |
} else { | |
callback(z); | |
} | |
}); |
Why is this “better”? Well, for one, adding error handling is straightforward:
var dostuffEmitter = new EventEmitter(); | |
dostuffEmitter.on('task1', function (callback) { | |
if (error) { | |
dostuffEmitter.emit('error', error); | |
return; | |
} | |
dostuffEmitter.emit('task2', x, callback); | |
}); | |
dostuffEmitter.on('task2', function (x, callback) { | |
if (error) { | |
dostuffEmitter.emit('error', error); | |
return; | |
} | |
dostuffEmitter.emit('task3', y, callback); | |
}); | |
dostuffEmitter.on('task3', function (y, callback) { | |
if (error) { | |
dostuffEmitter.emit('error', error); | |
return; | |
} | |
dostuffEmitter.emit('finish', z, callback); | |
}); | |
dostuffEmitter.on('finish', function (z, callback) { | |
if (z < 0) { | |
callback(0); | |
} else { | |
callback(z); | |
} | |
}); |
We can add telemetry like so:
dostuffEmitter.on('task2', function (x, callback) { | |
if (error) { | |
dostuffEmitter.emit('error', error); | |
return; | |
} | |
dostuffEmitter.emit('telemetry', someMetricHere); | |
dostuffEmitter.emit('task3', y, callback); | |
}); |
We can alter control flow without rewriting the “callback hell”:
dostuffEmitter.on('task1', function (callback) { | |
dostuffEmitter.emit('task3', x, callback); | |
}); | |
dostuffEmitter.on('task2', function (x, callback) { | |
dostuffEmitter.emit('finish', y, callback); | |
}); | |
dostuffEmitter.on('task3', function (y, callback) { | |
dostuffEmitter.emit('task2', z, callback); | |
}); |
In general, the EventEmitter will be a better idea. I hope you’ll find some of these hints interesting and find writing control flow in Node.js a bit more friendly.