Computer scienceBackendNode.jsCore ConceptsInternal modulesEventsEvents module basics

Working with events

10 minutes read

The events module in Node.js is a core module that provides the ability to create, fire, and manage events. It is built around the EventEmitter class, which is used to raise and handle custom events. In this topic, you will dive deeper into the events module and take a look at more advanced uses of EventEmitter.

Create a Message Manager

Let's follow an example to better understand how you can use event emitters in Node.js.

First, create a MessageManager class that extends EventEmitter:

const EventEmitter = require('events');

class MessageManager extends EventEmitter {
  constructor() {
    super(); // access to the methods and properties of EventEmitter
    this.messages = []; // a list of sent messages
    this.receivedMessages = []; // a list of all sent messages
  }
};

The constructor includes a this.messages property to simulate a message queue sent by users and this.receivedMessages array keeps a record of all received messages. It also calls super() to get access to the methods and properties of EventEmitter.

Now, add a sendMessage method after the constructor:

sendMessage(message) {
  this.messages.push(message);
  this.emit('messageSent', message);
}

This method takes a message string and after adding it to the messages array, it ends by emitting the same message with messageSent event.

Next, you need to be able to receive messages, so add a method called receiveMessage:

receiveMessage() {
  const message = this.messages.shift(); // Get the first element
  this.receivedMessages.push(message); // Store the received message
  this.emit('messageReceived', message);
}

// You can also add a method to return all the stored messages

getPreviousMessages() {
    return this.receivedMessages;
}

Listen for Events

First, you need to create an instance of the MessageManager class:

const messageManager = new MessageManager();

Next, add two listeners. One for the messageSent event and another for messageReceived:

// Callback function to handle message sent event
const messageSentListener = (message) => {
  console.log(`Message sent: ${message}`);
};

// Callback function to handle message received event
const messageReceivedListener = (message) => {
  console.log(`Message received: ${message}`);
};

// Event: messageSent
messageManager.on('messageSent', messageSentListener);
// Event: messageReceived
messageManager.on('messageReceived', messageReceivedListener);

The messageSent and messageReceived listeners simply log the messages to the console. Also, notice that you are capturing event data from the callback function. Here, the message argument is the data that the event emits.

Now use the sendMessage and receiveMessage functions to emit the events:

messageManager.sendMessage('Hello, world!');
messageManager.sendMessage('How are you?');

messageManager.receiveMessage(); // Output: Message received: Hello, world!
messageManager.receiveMessage(); // Output: Message received: How are you?

// you can also call the previousMessages to see all messages
const previousMessages = messageManager.getPreviousMessages();
console.log('Previous messages:', previousMessages);

Handle Error Events

In Node.js, the Event Emitter module provides a built-in mechanism for handling errors using the error event. When an error occurs within an Event Emitter instance, it emits the error event, allowing you to handle and respond to the error appropriately.

Let's go back to the receiveMessage function and add an error handler in case there is no message:

receiveMessage() {
  if (this.messages.length === 0) {
    this.emit('error', new Error('No message available'));
    return;
  }

  const message = this.messages.shift();
  this.receivedMessages.push(message); // Store the received message
  this.emit('messageReceived', message);
}

When an error occurs, you can emit the error event by calling the emit() method on the Event Emitter instance and passing an Error object as the argument.

The error event is a special event in the Event Emitter module, and emitting it triggers the error-handling mechanism.

Next, listen for the error event:

// Event: messageSent
...

// Event: messageReceived

...

// Callback function to handle error event
const errorListener = (error) => {
  console.error(`An error occurred: ${error.message}`);
};

// Event: error
messageManager.on('error', errorListener);

It's important to note that if you don't listen for the error event with an event listener, Node.js will throw an unhandled exception and terminate the process.

Now if we call receiveMessage without sending a message before:

messageManager.receiveMessage(); // Output: An error occurred: No message available

Manage Event Listeners

To manage event listeners Node.js provides certain methods. If you need to count how many listeners are active you can use listenerCount. So, going back to the MessageManager class, add a new method:

countEventListeners(eventName) {
  const listeners = this.listenerCount(eventName);
  console.log(`Number of listeners for '${eventName}': ${listeners}`);
}

The listenerCount method takes the event name as the argument and returns the count. So when we call the new method:

messageManager.countEventListeners('messageSent'); // Output: Number of listeners for 'messageSent': 1

Another important mechanism Node.js provides is removing the event listeners. This is important for several reasons:

  1. Memory management: When an event listener is registered, it creates a reference to the listener function. If the listener is not removed when it is no longer needed, it can lead to a memory leak. The Event Emitter will continue to hold a reference to the listener, preventing it from being garbage collected.

  2. Preventing unintended side effects: If an event listener is not removed and the associated event continues to be emitted, the listener function will be invoked even if it is no longer required. This can result in unintended side effects or unnecessary processing.

  3. Avoiding duplicate event handling: In some cases, you may dynamically add event listeners based on certain conditions or user interactions. If you don't remove the old event listeners before adding new ones, you may end up with multiple event listeners for the same event. This can lead to duplicate event handling and unexpected behavior.

  4. Performance optimization: If an application has a large number of event listeners registered, it can impact performance. Event emission involves iterating over the registered listeners and invoking each listener function. By removing unnecessary event listeners, you can reduce the number of listeners that need to be iterated over during event emission, resulting in improved performance.

To remove an event listener, you typically use the removeListener() method of the Event Emitter instance. Alternatively, you can use the off() method, which is an alias for removeListener().

It's important to remove the listener using the same function that was used to register it, ensuring the correct listener is removed.

Let's use the off method to remove listeners at the end of our program:

// Removing event listeners
messageManager.off('messageSent', messageSentListener);
messageManager.off('messageReceived', messageReceivedListener);

// Though it's not necessary, you can also remove the error listener
messageManager.off('error', errorListener);

Here, we use the off() method to remove the event listeners for the 'messageSent', 'messageReceived', and 'error' events. By passing the event name and the corresponding listener function to off(), we can remove the specific event listeners.

Removing event listeners using off() ensures that the listeners are no longer active and won't be invoked when the corresponding events are emitted.

You could also remove all of the listeners of an event with this method removeAllListeners():

// Inside MessageManager class
removeListeners(eventName) {
  this.removeAllListeners(eventName);
  console.log(`All listeners for '${eventName}' removed`);
}

...

messageManager.removeListeners('messageSent'); // Output: All listeners for 'messageSent' removed

Full program:

const EventEmitter = require('events');

class MessageManager extends EventEmitter {
  constructor() {
    super();
    this.messages = [];
    this.receivedMessages = [];
  }

  sendMessage(message) {
    this.messages.push(message);
    this.emit('messageSent', message);
  }

  receiveMessage() {
    if (this.messages.length === 0) {
      this.emit('error', new Error('No messages available'));
      return;
    }

    const message = this.messages.shift();
    this.receivedMessages.push(message); // Store the received message
    this.emit('messageReceived', message);
  }

  getPreviousMessages() {
    return this.receivedMessages;
  }

 countEventListeners(eventName) {
    const listeners = this.listenerCount(eventName);
    console.log(`Number of listeners for '${eventName}': ${listeners}`);
  }

  removeAllListeners(eventName) {
    this.removeAllListeners(eventName);
    console.log(`All listeners for '${eventName}' removed`);
  }
}

// Usage example
const messageManager = new MessageManager();

// Callback function to handle message sent event
const messageSentListener = (message) => {
  console.log(`Message sent: ${message}`);
};

// Callback function to handle message received event
const messageReceivedListener = (message) => {
  console.log(`Message received: ${message}`);
};

// Callback function to handle error event
const errorListener = (error) => {
  console.error(`An error occurred: ${error.message}`);
};

// Event: messageSent
messageManager.on('messageSent', messageSentListener);
// Event: messageReceived
messageManager.on('messageReceived', messageReceivedListener);
// Event: error
messageManager.on('error', errorListener);

messageManager.sendMessage('Hello, world!');
messageManager.sendMessage('How are you?');

messageManager.receiveMessage(); // Output: Message received: Hello, world!
messageManager.receiveMessage(); // Output: Message received: How are you?
messageManager.receiveMessage(); // Output: An error occurred: No messages available

const previousMessages = messageManager.getPreviousMessages();
console.log('Previous messages:', previousMessages);

messageManager.countEventListeners('messageSent'); // Output: Number of listeners for 'messageSent': 1

// Removing event listeners
messageManager.off('messageSent', messageSentListener);
messageManager.off('messageReceived', messageReceivedListener);
messageManager.off('error', errorListener);

The output of the program:

Message sent: Hello, world!
Message sent: How are you?
Message received: Hello, world!
Message received: How are you?
An error occurred: No messages available
Previous messages: [ 'Hello, world!', 'How are you?' ]
Number of listeners for 'messageSent': 1

Conclusion

In this topic, you've studied a complete example of an Event Emitter in Node.js by creating a custom emitter class called MessageManager. The program emits and listens to such events as messageSent, and messageReceived. The program also handles an error case with the special error event. Also, the program can manage the event listeners by counting how many active listeners there are and removing them once they are no longer needed to help with memory efficiency and prevent unintended side effects. Following this example program, you are ready to use this module in projects.

6 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo