How to use events in Node.js the right way

How to use events in Node.js the right way

  • 2018-11-21 01:42 PM
  • 107

How to use events in Node.js the right way, Before event-driven programming became popular, the standard way to communicate between different parts of an application was pretty straightforward:

a component that wanted to send out a message to another one explicitly invoked a method on that component. But event-driven code is written to react rather than be called.

The Benefits Of Eventing

This approach causes our components to be much more decoupled. As we continue to write an application, we identify events along the way. We fire them at the right time and attach one or more event listeners to each one. Extending functionality becomes much easier. We can add on more listeners to a particular event. We are not tampering with the existing listeners or the part of the application where the event was fired from. What we’re talking about is the Observer pattern.

https://www.dofactory.com/javascript/observer-design-pattern

Designing An Event-Driven Architecture

Identifying events is pretty important. We don’t want to end up having to remove/replace existing events from the system. This might force us to delete/modify any number of listeners that were attached to the event. The general principle I use is to consider firing an event only when a unit of business logic finishes execution.

So say you want to send out a bunch of different emails after a user’s registration. Now, the registration process itself might involve many complicated steps, and queries. But from a business point of view, it is one step. And each of the emails to be sent out are individual steps as well. So it would make sense to fire an event as soon as registration finishes. We have multiple listeners attached to it, each responsible for sending out one type of email.

Node’s asynchronous, event-driven architecture has certain kinds of objects called “emitters.” They emit named events which cause functions called “listeners” to be invoked. All objects that emit events are instances of the EventEmitter class. Using it, we can create our own events:

An Example

Let’s use the built-in events module (which I encourage you to check out in detail) to gain access to EventEmitter.

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

module.exports = myEmitter;

This is the part of the application where our server receives an HTTP request, saves a new user and emits an event:

const myEmitter = require('./my_emitter');

// Perform the registration steps

// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);

And a separate module where we attach a listener:

const myEmitter = require('./my_emitter');

myEmitter.on('user-registered', (user) => {
  // Send an email or whatever.
});

It’s a good practice to separate policy from implementation. In this case policy means which listeners are subscribed to which events. Implementation means the listeners themselves.


const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');


myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
module.exports = (user) => {
  // Send a welcome email or whatever.
}

This separation allows for the listener to become re-usable too. It can be attached to other events that send out the same message (a user object). It’s also important to mention that when multiple listeners are attached to a single event, they will be executed synchronously and in the order that they were attached. Hence someOtherListener will run after sendEmailOnRegistration finishes execution.

However, if you want your listeners to run asynchronously you can simply wrap their implementations with setImmediate like this:

module.exports = (user) => {
  setImmediate(() => {
    // Send a welcome email or whatever.
  });
}

Keep Your Listeners Clean

Stick to the Single Responsibility Principle when writing listeners. One listener should do one thing only and do it well. Avoid, for instance, writing too many conditionals within a listener that decide what to do depending on the data (message) that was transmitted by the event. It would be much more appropriate to use different events in that case:

const myEmitter = require('./my_emitter');

// Perform the registration steps

// The application should react differently if the new user has been activated instantly.
if (user.activated) {
  myEmitter.emit('user-registered:activated', user);
  
} else {
  myEmitter.emit('user-registered', user);
}
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');


myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);

myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);

Detaching Listeners Explicitly When Necessary

In the previous example, our listeners were totally independent functions. But in cases where a listener is associated with an object (it’s a method), it has to be manually detached from the events it had subscribed to. Otherwise, the object will never be garbage-collected since a part of the object (the listener) will continue to be referenced by an external object (the emitter). Thus the possibility of a memory leak.

For example, if we’re building a chat application and we want the responsibility for showing a notification when a new message arrives in a chat room that a user has connected to should lie within that user object itself, we might do this:

class ChatUser {
  
  displayNewMessageNotification(newMessage) {
    // Push an alert message or something.
  }
  
  // `chatroom` is an instance of EventEmitter.
  connectToChatroom(chatroom) {
    chatroom.on('message-received', this.displayNewMessageNotification);
  }

  disconnectFromChatroom(chatroom) {
    chatroom.removeListener('message-received', this.displayNewMessageNotification);
  }
}

When the user closes his/her tab or loses their internet connection for a while, naturally, we might want to fire a callback on the server-side that notifies the other users that one of them just went offline. At this point, of course, it doesn’t make any sense for displayNewMessageNotification to be invoked for the offline user. It will continue to be called on new messages unless we remove it explicitly. If we don’t, aside from the unnecessary call, the user object will also stay in memory indefinitely. So be sure to call disconnectFromChatroom in your server-side callback that executes whenever a user goes offline.

Beware

The loose coupling in event-driven architectures can also lead to increased complexity if we’re not careful. It can be difficult to keep track of dependencies in our system. Our application will become especially prone to this problem if we start emitting events from within listeners. This could possibly trigger chains of unexpected events.

ChatBots: Messenger ChatBot with API.AI and Node.JS

Master the MEAN Stack - Learn By Example

Node.js - From Zero to Web App

Typescript Async/Await in Node JS with testing

Projects in Node.js - Learn by Example