Using an Event Emitter — Common Use and Edge Cases


This is the fourth article from a new series about Node.js. In the last few chapters we covered some ways of controlling the flow of asynchrnous operations: the callback pattern, using the async package to orchestrate them and using an in-memory work queue. In this article we'll cover a new way of managing the flow of your code, this time using events. Enjoy!

- Pedro Teixeira, CTO, YLD!

(Some of the code samples in this article you can find in this github repo).

Using an Event Emitter

The callback pattern works well for simple operations that have a start and an end state; but if you’re interested in state changes, a callback is not enough. Fortunately Node.js ships with an event emitter implementation. The event emitter pattern allows you to decouple the producers and consumers of events using a standard interface.

Consuming events

Let’s say that your Node.js process is somehow able to connect to a home automation system that can notify you of several events happening in your home. If the system can notify you of the main doorbell ringing, a door object could emit an event named “bell”. Any module could subscribe to doorbell rings by listening to that event on a given object:

var home = require('./home');
home.door.on('bell', function() {    
  /// do something  
});

In this case the home.door object is an event emitter. As a client of this object, you start listening to "bell" events by using the .on method and passing in a function that gets called whenever an event with that specific name happens.

Events can also bear some data. For instance, let’s say that you want to get notified when a light bulb is turned on or off:

home.lights.on('on', function(number, room) {    
  console.log('light number %d in room %s was turned on', number, room);  
});
home.lights.on('off', function(number, room) {    
  console.log('light number %d in room %s was turned off', number, room);  
});

Here you can see that the “on” and the “off” events bear the room name. This is arbitrary data that is emitted by the emitter.

Besides subscribing to events, you can also unsubscribe by using listener.removeListener, specifying the event type and the event listener function. To use it you will have to name the listener function like this:

function onTurnLightOn(number, room) {    
  console.log('light number %d in room %s was turned on', number, room);  
}
home.lights.on('turnon', onTurnLightOn);
/// later...
home.lights.removeListener('turnon', onTurnLightOn);
// \`onTurnLightOn\` doesn't get called after this

Producing events

You can use the event emitter as a producer by constructing a JavaScript pseudo-class and use Node’s built-in util.inherits to setup the proper inheritance attributes. Here is an example of a clock event emitter that emits tic and toc events:

clock_emitter_class.js:

var inherits = require('util').inherits;    
var EventEmitter = require('events').EventEmitter;
module.exports = Clock;
function Clock() {    
  if (! (this instanceof Clock)) return new Clock();
  this.\_started = false;
  EventEmitter.call(this);  
}
inherits(Clock, EventEmitter);
Clock.prototype.start = function start() {    
  var self = this;
  if (self.\_started) return;
  var tic = true;
  this.\_started = Date.now();
  self.\_interval = setInterval(function() {  
    var event = tic ? 'tic' : 'toc';  
    self.emit(event, self.time());  
    tic = ! tic;  
  }, 1000);  
};
Clock.prototype.stop = function stop() {    
  clearInterval(this.\_interval);  
  this.\_started = false;  
};
Clock.prototype.time = function() {    
  return this.\_started && Date.now() - this.\_started;  
};

Here you can see that the clock itself is an event emitter, and it’s internally calling self.emit to emit arbitrary events (tic and toc in this case). Alongside each event, we're passing the current clock time (in milliseconds since the time it was started) by using the arguments in self.emit following the event name.

Clients of this Clock class can then do:

var Clock = require('./clock');    
var clock = Clock();
clock.on('tic', function(t) {    
  console.log('tic:', t);  
});
clock.on('toc', function(t) {    
  console.log('toc:', t);  
});
clock.start();
// stop the clock 10 seconds after  
setTimeout(function() {    
  clock.stop();  
}, 10e3)

How an event emitter handles errors

As we’ve seen, an event type is defined by an arbitrary string. In that aspect, all events are treated equally, with one exception: errors.

If an event emitter emits one event for which it has no listeners, that event gets ignored. But, in the special case of the event name being “error”, the error is instead thrown into the event loop, generating an uncaught exception.

If that happens, you can catch an uncaught exception by listening to the uncaughtException that the global process object emits:

process.on('uncaughtException', function(err) {    
  console.error('uncaught exception:', err.stack || err);  
  // orderly close server, resources, etc.  
  closeEverything(function(err) {  
    if (err)  
      console.error('Error while closing everything:', err.stack || err);
    // exit anyway  
    process.exit(1);  
  });  
});

When catching an uncaught exception, you should treat it as a permanent failure in your Node process: consider taking this opportunity to close the services in an orderly fashion, and eventually terminate the process.

Yes, shutdown your process when an uncaught exception occurs. Otherwise you will most probably end up with a resource leak due to the unknown state some Node objects will be in.

Listening to events once

What happens when, in the above example, more than one uncaught exception gets caught? Both will trigger the closeEverything function, which may bring problems on your shutdown procedure. In this case you should probably only consider the first instance of an uncaught exception happening. You can do that by using emitter.once:

process.once('uncaughtException', function(err) {    
  // orderly close server, resources, etc.  
  closeEverything(function(err) {  
    if (err)  
      console.error('Error while closing everything:', err.stack || err);
    // exit anyway  
    process.exit(1);  
  });  
});

But now what happens when two uncaughtExceptions occur? The second one will find no event listener, and will trigger an immediate process shutdown, interrupting our beloved closeEverything that was taking care of an orderly shutdown.

To avoid that we can still log every uncaught exception we get meantime by adding a second listener:

process.on('uncaughtException', function(err) {    
  console.error('uncaught exception:', err.stack || err);  
});

Synchronous event delivery

Node.js is asynchronous, but since no I/O is envolved in emitting an event, event delivery is treated synchronously.

emit_asynchronous.js:

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.on('beep', function() {    
  console.log('beep');  
});
emitter.on('beep', function() {    
  console.log('beep again');  
});
console.log('before emit');
emitter.emit('beep');
console.log('after emit');

If run this file you will get the following output:

before emit    
beep    
beep again    
after emit

So, when emitting events, bear in mind that the listeners will be called before emitter.emit returns.

Remembering this last bit can save you a lot of debugging time when using more than one listener for the same event.

Event delivery exceptions

Since the event delivery is synchronous to the emission, what happens when one listener accidently throws an exception? Let’s see:

emit_throws.js:

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.on('beep', function() {    
  console.log('beep');  
});
emitter.on('beep', function() {    
  throw Error('oops!');  
});
emitter.on('beep', function() {    
  console.log('beep again');  
});
console.log('before emit');
try {    
  emitter.emit('beep');  
} catch(err) {  
  console.error('caught while emitting:', err.message);  
}
console.log('after emit');

Running the code above will produce the following output:

before emit    
beep    
caught while emitting: oops!    
after emit

This is bad news. It means that, when emitting events, if one of the listeners throws, depending on the order of the listener registration, the listener may not be notified. This means that the event emitter is a good pattern for logically decoupling producers and consumers at the API level, but in practice the event producers and consumers are somewhat coupled.

For a more decoupled approach you can use an in-memory queue, or even a persistent queue (to be discussed in a future article).

Next article

If you need to process a stream of data from a source, you need to create one such stream, or even repeatedly transform some data, Node has that abstraction for you: Streams. In the next article we’ll cover the several types of streams, their interface, how you can create them and then assemble these reusable streams to form data processing pipelines.

You can find all the previous posts on Flow Control here:

Written by: Pedro Teixeira

This article was extracted from the Flow Control Patterns, a book from the Node Patterns series.

Originally published at blog.yld.io on December 15, 2015.


Written by YLDDecember 2nd, 2015


Share this article