Understanding the Node.js Event Loop

Node's "event loop" is central to being able to handle high throughput scenarios. It is a magical place filled with unicorns and rainbows, and is the reason Node can essentially be "single threaded" while still allowing an arbitrary number of operations to be handled in the background. This post will shed light on how the event loop operates so you too can enjoy the magic.

Event Driven Programming #

The first thing needed in order to understand the event loop is an understanding of the event-driven programming paradigm. This has been well understood since the 1960's. Today, event-driven programming is largely used in UI applications. A major usage of JavaScript is to interact with the DOM, so the use of event-based APIs was natural.

Defined simply: event-driven programming is application flow control that is determined by events or changes in state. The general implementation is to have a central mechanism that listens for events and calls a callback function once an event has been detected (i.e. state has changed). Sound familiar? It should. That's the basic principle behind Node's event loop.

For those familiar with client-side JavaScript development, think of all the .on*() methods, such as element.onclick(), that are used in conjunction with DOM Elements to convey user interaction. This pattern works well when a single item can emit many possible events. Node uses this pattern in the form of the EventEmitter, and is located in places such as Server, Socket and the 'http' module. It's useful when we need to emit more than one type of state change from a single instance.

Another common pattern is succeed or fail. There are two common implementations around today. First is the "error back" callback style, where the error of the call is the first argument passed to the callback. The second has emerged with ES6, using Promises.

The 'fs' module mostly uses the error back callback style. It would technically be possible to emit additional events for some calls, such as fs.readFile(), but the API was made to only alert the user if the desired operation succeeded or if something failed. This API selection was an architecture decision and not due to technical limitations.

A common misconception is that event emitters are somehow asynchronous in nature on their own, but this is incorrect. The following is a trivial code snippet to demonstrate this.

function MyEmitter() {
  EventEmitter.call(this);
}
util.inherits(MyEmitter, EventEmitter);

MyEmitter.prototype.doStuff = function doStuff() {
  console.log('before')
  emitter.emit('fire')
  console.log('after')}
};

var me = new MyEmitter();
me.on('fire', function() {
  console.log('emit fired');
});

me.doStuff();
// Output:
// before
// emit fired
// after

EventEmitter often appears asynchronous because it is regularly used to signal the completion of asynchronous operations, but the EventEmitter API is entirely synchronous. The emit function may be called asynchronously, but note that all the listener functions will be executed synchronously, in the order they were added, before any execution can continue in statements following the call to emit.

Mechanical Overview #

Node itself depends on multiple libraries. One of those is libuv, the magical library that handles the queueing and processing of asynchronous events. For the remainder of this post please keep in mind that I won't distinguish if a point made relates directly to Node or libuv.

Node utilizes as much of what's already available from the operating system's kernel as possible. Responsibilities like making write requests, holding connections and more are therefore delegated to and handled by the system. For example, incoming connections are queued by the system until they can be handled by Node.

You may have heard that Node has a thread pool, and might be wondering "if Node pushes all those responsibilities down why would a thread pool be needed?" It's because the kernel doesn't support doing everything asynchronously. In those cases Node has to lock a thread for the duration of the operation so it can continue executing the event loop without blocking.

Here is a simplified diagram to explain the mechanical overview of when things run:

diagram

A couple important notes about the inner workings of the event loop that would be difficult to include in the diagram are:

Event Emitter and the Event Loop #

To simplify interaction with the event loop the EventEmitter was created. It is a generic wrapper that more easily allows creating event-based APIs. Because of some of the confusion that surrounds how these two interact we'll now address common points that tend to trip up developers.

The following example shows how forgetting that emitting events happens synchronously can cause events to be missed by the user.

// Post v0.10, require('events').EventEmitter is not necessary.
var EventEmitter = require('events');
var util = require('util');

function MyThing() {
  EventEmitter.call(this);

  doFirstThing();
  this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);

var mt = new MyThing();

mt.on('thing1', function onThing1() {
  // Sorry, never going to happen.
});

The flaw with the above is that 'thing1' can never be captured by the user because MyThing() must finish instantiating before listening for any events. Here is a simple solution that also doesn't require any additional closures:

var EventEmitter = require('events');
var util = require('util');

function MyThing() {
  EventEmitter.call(this);

  doFirstThing();
  setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);

function emitThing1(self) {
  self.emit('thing1');
}

var mt = new MyThing();

mt.on('thing1', function onThing1() {
  // Whoot!
});

The following would also work, but at a drastic performance cost:

function MyThing() {
  EventEmitter.call(this);

  doFirstThing();
  // Using Function#bind() makes the world much slower.
  setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);

Another problem case is with emitting errors. Figuring out problems with your application can be hard enough, but losing the call stack can make it impossible. A call stack is lost when an Error is instantiated on the far end of an asynchronous request. The two most reasonable solutions to get around this problem are to emit synchronously or to make sure other important information propagates with the error. The following example shows each one being used:

MyThing.prototype.foo = function foo() {
  // This error will be emitted asynchronously.
  var er = doFirstThing();
  if (er) {
    // The error needs to be created immediately to preserve
    // the call stack.
    setImmediate(emitError, this, new Error('Bad stuff'));
    return;
  }

  // Emit the error immediately so it can be handled.
  var er = doSecondThing();
  if (er) {
    this.emit('error', 'More bad stuff');
    return;
  }
}

Consider the situation. It may be possible that the error being emitted should be handled immediately, before the application proceeds executing. Or it may be something as trivial as a bad argument that needs to be reported and can easily be handled later. Also, it's not a good idea to have a constructors that emit errors, since the object instance's construction may very well be incomplete. Just throw an exception in that case.

Wrapping Up #

This post has been very light on the technical details and inner workings of the event loop. Which was deliberate. That information will be covered in the future, but first we needed to make sure every one was on the same page with these basics. Look forward to another article on how the event loop interacts with your system's kernel to achieve the asynchronous magic that allows Node to run.