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);
});
});
});
}

view raw

gistfile1.js

hosted with ❤ by GitHub

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);
}
});

view raw

gistfile1.js

hosted with ❤ by GitHub

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);
}
});

view raw

gistfile1.js

hosted with ❤ by GitHub

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);
});

view raw

gistfile1.js

hosted with ❤ by GitHub

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);
});

view raw

gistfile1.js

hosted with ❤ by GitHub

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.

Leave a comment