NodeJS – EventEmitters

Event Emitters – For Building Highly Customizable Components

The Event Emitters allows us to build highly customizable components. By sending the events at the key points in our program execution, we let the clients add their custom logic externally at those points.

In short, the events provide easy to use customization points for the clients. Because of this, most of the Nodejs APIs implement these event emitters to keep their usage simple and customizable.

Understanding the Benefits with an Example:

Here is an example of a readable stream that implements EventEmitters. The highlighted lines shows three key events from this component, namely ‘data’, ‘end’, ‘error’.

The comments within each shows, how the events help us customize the core logic across the readable stream at a few simplified points.

const fs = require('fs');
const rs = fs.createReadStream('example.txt','utf8');

rs.on('data', (chunk)=>{
    // Here is another chunk of data collected for you....
    // You focus on doing what you need to do. For example :
    // 1. You may pipe it to another stream.
    // 2. You may do some inflate or deflate operation on it    
});

rs.on('end', () => {
  // We have just finished reading all our data!
  // Do you have anything specific in your mind to do here ?
});

rs.on('error', (err) => {
  // The error might occur at different points inside the readable component..
  // But, don't worry, I will report all the errors here for you to handle
});

Just by implementing these event handlers we will have a customized version of the readable stream ready for our specific need.

 

How to Create an Event Emitter

Making any class an event emitter is simple. We just need to extend the EventEmitter class from the ‘events’ module, as in line-3.

Now once we create an instance of the class, we can add the handlers for the events we want to customize.

const EventEmitter = require('events');

//Making a Bird class that is capable of emitting events
class Bird extends EventEmitter {}

//1. Creating an instance for a specific use
const myDuck = new Bird();

//2. Adding sound event handler : What sound should it make on sound event?
myDuck.on('sound', () => {
  console.log('quack...quack...quack...');
});


//Emit different events to test
myDuck.emit('sound');   // prints: quack...quack...quack...

myDuck.emit('move');    //It will be ignored as there is no handler

myDuck.emit('sound');   // prints: quack...quack...quack...
const EventEmitter = require('events');

//Making a Bird class that is capable of emitting events
class Bird extends EventEmitter {

    wayBackHome(){
          this.emit('sound');
          //Other processing logic, if any
          this.emit('move');
          //SomeOther processing logic, if any
          this.emit('sound');
    }
}

//Creating an instance and add required event handlers
const myDuck = new Bird();

myDuck.on('sound', () => {
  console.log('quack...quack...quack...');
});

//This is going to emit events 
myDuck.wayBackHome();

As specified in the comments, running this class will print : ‘quack…quack…quack…’ twice and ignore the ‘move’ event since it does not have a handler.

Just for reference, the second tab above shows how a real class emits such events from its operations.

 

 

Exploring EventEmitters with Examples

1. Handle an event only once

The on(‘event’) handles the events on each occurrence. In case we want to handle an event only once, we can use once(‘event’) as shown on line-8.

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

myEmitter.on('event', () => { //will print thrice
     console.log('On handler ...'); 
}); 
myEmitter.once('event', () => {//will print only once
     console.log('Once handler ...'); 
}); 

myEmitter.emit('event');
myEmitter.emit('event');
myEmitter.emit('event');

Output:

On handler ...
Once handler ...
On handler ...
On handler ...

 

2. How to pass data along with the event?

As shown below, we can pass a list of data elements as comma separated parameters while emitting events.

const EventEmitter = require('events');
const myEmitter = new EventEmitter();

myEmitter.on('event', () =>{
  console.log('Not using data.');
});
myEmitter.on('event', (arg1) => {
  console.log(`First data : ${arg1}`);
});
myEmitter.on('event', (...args) => {
  console.log(`All data : ${args}`);
});

myEmitter.emit('event', 1, 2, 3, 4, 5);

Output:

Not using data.
First data : 1
All data : 1,2,3,4,5

 

3. How to throw and handle an error?

In case we throw an error, the client need to handle it using a try catch block. But, in case of an EventEmitter, instead of throwing an error, we can emit the error using an ‘error’ event as shown in line-9 below. It will have the same effect, had we simply thrown that error.

To handle this error, we can add an event handler as shown in line-4.

const { EventEmitter} = require('events');
const myEmitter = new EventEmitter();

myEmitter.on('error',(err)=>{
    console.log("Error handled: "+ err.message);
});

//Instead of throwing the error, we can emit using 'error' event
myEmitter.emit('error', new Error('Oooops! Something went wrong.'));
console.log("Program won't reach here without handling the error event!");

Output:

Error handled: Oooops! Something went wrong.
Program won't reach here without handling the error event!

 

4. How to capture an error for monitoring purpose

The events module provides an utility called, errorMonitor; just to capture the error without handling it. We can use this to collect the error for some monitoring tool.

The line-6 shows the use of this utility. With line-4 commented out, the following program should crash.

const { EventEmitter, errorMonitor } = require('events');
const myEmitter = new EventEmitter();

//myEmitter.on('error',(err)=>{console.log("Error handled: "+ err.message);});

myEmitter.on(errorMonitor, (err) => {
  //I am not handling the error,
  //just capturing it to a monitoring tool
});

//Instead of throwing the error, we can emit using 'error' event
myEmitter.emit('error', new Error('Oooops! Something went wrong.'));
console.log("Program won't reach here unless we handle the error event!");

 

5. Utility Functions for Managing Listeners

The event handlers that we add are the listeners for the events. The events module provide various apis for functions such as counting, listing, removing these listeners.

The table below lists some of those key functions.

FunctionsUsage
listenerCount(myEmitter,’some-event’) Provides the count of handlers for event ‘some-event’.
myEmitter.eventNames()Provides the event names for which we have listeners.
myEmitter.listeners(‘some-event’)Provides the listners functions registered for the event ‘some-event’
myEmitter.getMaxListeners()Provides max number of listeners we can register for the emitter.
The default is 10. We can change it using setMaxListeners
myEmitter.removeListener(‘event’, myListener) This will remove the last matching listener in the listener array for the event.
myEmitter.addListener(‘event’, myListener) Same as on(‘event’, myListener)
Adds the listener at the end of the listener array for the given event.
myEmitter.prependListener(‘event’, myListener)This allows us to add the listener at the start of the listener array.
Utility functions for managing listeners

The following program shows the usage of these functions :

const { EventEmitter, listenerCount } = require('events');
const myEmitter = new EventEmitter();

const myListener1 = () => { console.log('myListener1'); }
const myListener2 = () => { console.log('myListener2'); }

myEmitter.on('some-other-event', () =>{});

myEmitter.on('event', myListener1);
myEmitter.on('event', myListener2);
myEmitter.on('event', myListener1);

console.log("listenerCount for event :"+ listenerCount(myEmitter,'event'));
console.log("listenerCount for some-other-event :" + listenerCount(myEmitter, 'some-other-event'));
console.log("myEmitter.eventNames :"+ myEmitter.eventNames());
console.log("myEmitter.listeners :" + myEmitter.listeners('event'));

console.log("myEmitter.getMaxListeners :" + myEmitter.getMaxListeners());

myEmitter.removeListener('event', myListener1);
console.log("myEmitter.removeListener('event', myListener1) - Done")
console.log("myEmitter.listeners : " + myEmitter.listeners('event'));

myEmitter.prependListener('event', myListener2);
console.log("emitter.prependListener('event', listner2) - Done")
console.log("myEmitter.listeners : " + myEmitter.listeners('event'));

Output:

F:\nodejs\node01\samples\events>node 05-multiple-listners.js
listenerCount for event :3
listenerCount for some-other-event :1

myEmitter.eventNames :some-other-event,event
myEmitter.listeners :() => { console.log('myListener1'); },() => { console.log('myListener2'); },() => { console.log('myListener1'); }

myEmitter.getMaxListeners :10

myEmitter.removeListener('event', myListener1) - Done
myEmitter.listeners : () => { console.log('myListener1'); },() => { console.log('myListener2'); }

emitter.prependListener('event', listner2) - Done
myEmitter.listeners : () => { console.log('myListener2'); },() => { console.log('myListener1'); },() => { console.log('myListener2'); }