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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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”:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.