DEV Community

Cover image for đŸ”¥Mastering Event Emitters in Node.js
angDecoder
angDecoder

Posted on

đŸ”¥Mastering Event Emitters in Node.js

Table of content

1. Intro
2. What is event Emitter?
3. Why Event Emitters are required?
4. Some additional functionalities.
5. Error handling
6. Custom Event Emitter



1. Intro

In the world of Node.js, Event Emitters are the backbone of asynchronous communication, enabling seamless interaction between different components of an application. In this guide, we'll delve into the essence of Event Emitters, exploring how they empower developers to create scalable and responsive systems.


2. What is Event Emitter?

Events --> Events in programming generally refer to occurrences or happenings within a system or application that trigger specific actions or reactions.

Event Emitters --> These are objects which allows for the handling and propagation of events within a Node.js application.

In simple words event emitters are objects which keep track of events and all the functions attached to it. Whenever a event is emitted all the functions attached to that particular events are emitted synchronously. Let us see that in action :


const EventEmitter = require('node:events');

class MyEmitter extends EventEmitter {};

const myEmitter = new MyEmitter();

// registering an event
myEmitter.on('foo', () => {
  console.log('an event occurred!');
});

// emitting a event
myEmitter.emit('foo'); 

/*
OUTPUT :
an event occurred!
*/
Enter fullscreen mode Exit fullscreen mode

EventEmitter object exposes two function .on() and .emit(). We can attach multiple functions to the same event and they are called synchronously one by one in the order they were registered.

const EventEmitter = require('node:events');

class MyEmitter extends EventEmitter {};

const myEmitter = new MyEmitter();

myEmitter.on('foo', () => {
  console.log('foo -- 1');
});

myEmitter.on('foo', () => {
  console.log('foo -- 2');
});

myEmitter.on('foo', () => {
  console.log('foo -- 3');
});

myEmitter.emit('foo'); 
/*
OUTPUT :
foo -- 1
foo -- 2
foo -- 3
*/

Enter fullscreen mode Exit fullscreen mode

3. Why Event Emitters are required?

This particular question can be understood with a simple example. Let us say we want to perform some action whenever we press 'ENTER' on keyboard.

One solution could be that, we synchronously(constantly) check if the 'ENTER' is pressed or not. But the problem with this approach is that we would be dedicating one core of out CPU to entirely this task. This core would not be able to perform any other task other than checking for 'ENTER' key press on keyboard.

Another solution, which the Node.js follows, is event-driven architechture. In this approach, whenever the 'ENTER' key is pressed in keyboard, we emit an event which is read by CPU and performs the task accordingly. We don't need to synchronously check for the event, but whenever the event is emitted then only perform the action.


4. Some additional functionalities .

  • Passing arguments and 'this' to listeners

The .emit() method allows an arbitrary set of arguments to be passed to the listener functions.

const EventEmitter = require('node:events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('foo', function(a, b) {
  console.log(a, b, this === myEmitter); // this binded to myEmitter
  // OUTPUT : 1 2 true
});

myEmitter.on('foo',(a,b)=>{
  console.log(a,b,this===myEmitter); // this is binded not binded to myEmitter
});
myEmitter.emit('foo', 1, 2);

Enter fullscreen mode Exit fullscreen mode

By default, for normal function the 'this' is binded to 'myEmitter'. But, in case of arrow function 'this' is not binded to 'myEmitter' object.

  • Handling events only once

When a listener is registered using the .on() method, that listener is invoked every time the named event is emitted.
But, when it is registered using the .once() method it is called only once and ignored in subsequent calls.


const EventEmitter = require('node:events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();

myEmitter.once('foo', () => {
  console.log("event occured");
});
myEmitter.emit('foo');
// Prints: 1
myEmitter.emit('foo');
// Ignored

Enter fullscreen mode Exit fullscreen mode
  • Getting all the events

Using the .eventNames() method we can get all the registered listeners as an array of strings or symbols


const EventEmitter = require('node:events');

class MyEmitter extends EventEmitter{};
const ee1 = new MyEmitter();

ee1.on('foo',()=>{
  console.log('foo called');
});

ee1.on('bar',()=>{
  console.log('bar called');
});

console.log(ee1.eventNames());
// OUTPUT : [ 'foo' , 'bar' ]

Enter fullscreen mode Exit fullscreen mode

There are some other functionalities which are listed below which I have not explained because they are self explanatory.

  • .newListener(eventName,listener) --> adds new listenter just like .on() method

  • .removeListener(eventName) --> removes listener just like .off() method

  • .listenerCount(eventName) --> returns number of listener for particular event

  • .prependListener(eventName,listener) --> add listener to the begining of the list

  • .prependOnceListener(eventName,listener) --> add once listener to the beginning of the list

  • .removeListener(eventName,listener) --> removes listener from eventName

  • .removeAllListener(eventName) --> removes all listeners for particualr event


5. Error handling

When an error occurs within the EventEmitter instance, the typical action is for an 'error' event to be emitted.

If an EventEmitter doesn't have listener function for the 'error' event then : the error is thrown, the error stack is printed and Node.js process exits.

  "use strict";
const EventEmitter = require('events');
class MyEmitter extends EventEmitter{};

const myEmitter = new MyEmitter();


myEmitter.on('foo',()=>{
  myEmitter.emit('error', new Error('whoops! error occured'));
});

myEmitter.on('error',(err)=>{
  console.log('error occured ---> ',err);
});

myEmitter.emit('foo');
Enter fullscreen mode Exit fullscreen mode
  • Capture rejections of promises

Using async function with the event emitter can lead to unhandled rejection in case of thrown exception or rejected promise.


const EventEmitter = require('node:events');

class MyEmitter extends EventEmitter{};
const ee1 = new MyEmitter();

ee1.on('foo',async()=>{
  throw new Error('some error '); 
})

// does not capture thrown exception or rejected promise
// inside the 'foo' event
ee1.on('error',(err)=>{
  console.log('error here ---> ', err); 
});

ee1.emit('foo');

Enter fullscreen mode Exit fullscreen mode

Passing the "captureRejection" as "true" solves this default behaviour and 'error' event can now capture any unhanled thrown exception or rejected promise.


const EventEmitter = require('node:events');

class MyEmitter extends EventEmitter{};
const ee2 = new MyEmitter({ captureRejections : true });

ee2.on('foo',async()=>{
  // could be a thrown exception as well 
  // like in above exmaple
  return Promise.reject("my rejection");
})

// it can capture thrown exception or rejected promise
// inside the 'foo' event
ee2.on('error',(err)=>{
  console.log('error here ---> ', err); 
});

ee2.emit('foo');

Enter fullscreen mode Exit fullscreen mode

Setting events.captureRejections = true will change the default for all new instances of EventEmitter.


const events = require('node:events');
events.captureRejections = true;
// now the default behaviour is changed
// all the event emitter extending would now be
// able to capture rejections

Enter fullscreen mode Exit fullscreen mode

6. Custom Event Emitter

Now, that we have understood about the event emitters, let us now design our own event emitter.
At first, we will create a simple Node.js file which will have all the logic to implement our own event emitter. The new file will look something like this :

// FILE NAME --> events.js
module.exports = class Events{
    #listeners = {} 
    // use above object to keep track of 
   // events and corresponding listeners

    addListener(eventname, fun){
        // write logic here
    }

    on(eventname,fun){
        // write logic here
    }

    once(eventname,fun){
        // write logic here
    }

    removeListener(eventname,fun){
        // write logic here
    }

    off(eventname,fun){
        // write logic here
    }

    emit(eventname,...args){
        // write logic here
    }

    listenerCount(eventname){
        // write logic here
    }
}

Enter fullscreen mode Exit fullscreen mode

I will encourage you to write the logic yourself first and try that out.

Let us now add logic for adding new event listeners. For each listener we also need to keep the information whether it should be called once. To do that, we create a object with two properties :

  • once --> to check if it should be called only once
  • fun --> the listener function

module.exports = class Events{
    #listeners = {}

    addListener(eventname, fun, once = false){
        this.#listeners[eventname] = this.#listeners[eventname] || [];
        this.#listeners[eventname].push({once, fun });

        return this;
    }

    on(eventname,fun){
        return this.addListener(eventname,fun);
    }

    once(eventname,fun){
        return this.addListener(eventname,fun,true);
    }
}

Enter fullscreen mode Exit fullscreen mode

Now, let us add the functionality to remove the listeners from event emitter. Here we simply use filter function to remove the listener function.

REMINDER : For functions only reference is checked while comparing so pass the correct listener function while removing the function


module.exports = class Events{
    #listeners = {}

    removeListener(eventname,fun){
        if( !this.#listeners[eventname] )
            return this;


        this.#listeners[eventname] = this.#listeners[eventname].filter(elem=>{
            if( elem.fun!=fun )
                return true;

            return false;
        })

        return this;
    }

    off(eventname,fun){
        return this.removeListener(eventname,fun);
    }

  // other methods
}

Enter fullscreen mode Exit fullscreen mode

Finally, let us implement the '.emit()' and '.listenerCount()'. Inside '.emit()' method, if the listener is registered as 'once = true' then also remove that listener.

module.exports = class Events{
    #listeners = {}

    emit(eventname,...args){
        if( !this.#listeners[eventname] )
            return this;

        this.#listeners[eventname] = this.#listeners[eventname].filter(elem=>{
            elem.fun(...args);
            return !elem.once;
        })

        return this;
    }

    listenerCount(eventname){
        let fns = this.#listeners[eventname] || [];
        return fns.length;
    }

   // other methods

}

Enter fullscreen mode Exit fullscreen mode

Here is the entire code.


module.exports = class Events{
    #listeners = {}

    addListener(eventname, fun, once = false){
        this.#listeners[eventname] = this.#listeners[eventname] || [];
        this.#listeners[eventname].push({once, fun });

        return this;
    }

    on(eventname,fun){
        return this.addListener(eventname,fun);
    }

    once(eventname,fun){
        return this.addListener(eventname,fun,true);
    }

    removeListener(eventname,fun){
        if( !this.#listeners[eventname] )
            return this;


        this.#listeners[eventname] = this.#listeners[eventname].filter(elem=>{
            if( elem.fun!=fun )
                return true;

            return false;
        })

        return this;
    }

    off(eventname,fun){
        return this.removeListener(eventname,fun);
    }

    emit(eventname,...args){
        if( !this.#listeners[eventname] )
            return this;

        this.#listeners[eventname] = this.#listeners[eventname].filter(elem=>{
            elem.fun(...args);
            return !elem.once;
        })

        return this;
    }

    listenerCount(eventname){
        let fns = this.#listeners[eventname] || [];
        return fns.length;
    }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)