DEV Community

Cover image for Using Events In Node.js The Right Way
Usama Ashraf
Usama Ashraf

Posted on

Using 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. Basically, as we continue to write an application we’ll identify events along the way, fire them at the right time and attach one or more event listeners to each one. Extending functionality becomes much easier since we can just add on more listeners to a particular event without tampering with the existing listeners or the part of the application where the event was fired from. What we’re talking about is essentially the Observer pattern.


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

Designing An Event-Driven Architecture

Identifying events is pretty important since we don’t want to end up having to remove/replace existing events from the system, which 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, queries etc. 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 and have multiple listeners attached to it, each of which is responsible for sending out one type of email.

Node’s asynchronous, event-driven architecture has certain kinds of objects called “emitters” that 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.



// my_emitter.js

const EventEmitter = require('events');

const myEmitter = new EventEmitter();

module.exports = myEmitter;


Enter fullscreen mode Exit fullscreen mode

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



// registration_handler.js

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);


Enter fullscreen mode Exit fullscreen mode

And a separate module where we attach a listener:



// listener.js

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

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


Enter fullscreen mode Exit fullscreen mode

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



// subscriptions.js

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);


Enter fullscreen mode Exit fullscreen mode


// send_email_on_registration.js

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


Enter fullscreen mode Exit fullscreen mode

This separation allows for the listener to become re-usable too i.e. 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:



// send_email_on_registration.js

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


Enter fullscreen mode Exit fullscreen mode

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:



// registration_handler.js

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);
}


Enter fullscreen mode Exit fullscreen mode


// subscriptions.js

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);


Enter fullscreen mode Exit fullscreen mode

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 that 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:



// chat_user.js

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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, but 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 i.e. which listeners end up executing on which events. Our application will become especially prone to this problem if we start emitting events from within listeners, possibly triggering chains of unexpected events.

Top comments (11)

Collapse
 
jochemstoel profile image
Jochem Stoel

I'll flesh it out a bit more by providing some related details.

There are a few differences between the EventTarget in the browser and the EventEmitter from Node.

The EventEmitter class implements on, off and emit to register/unregister/fire events. In the EventTarget of window these are called addEventListener, removeEventListener and dispatchEvent.

The EventEmitter is chainable, the EventTarget in the browser is not.

// this works
emitter.on('user-registered', user => {
  console.log('new user registered!', user)
}).on('message', message => {
  console.log('received message!', message)
})

// this not works
document.body.addEventListener('DOMContentLoaded', console.log).addEventListener('error', console.error)
Enter fullscreen mode Exit fullscreen mode

Although on is chainable, off is not. When you remove an event listener using off You can not add another .off to it. In my opinion this is inconsistent and bad practice but I don't want to debate it.

Using EventEmitter, you simply emit any type of object. In a window, EventTarget.dispatchEvent requires the second argument to be of type Event. This means that in order to emit a new event, in the browser you need to instantiate an Event for it.

document.body.dispatchEvent('ready', new Event('something is ready'))
Enter fullscreen mode Exit fullscreen mode

If you want to deal with events in an even more sophisticated matter then have a look at eventemitter2 on NPM. It normalizes some inconsistencies and adds a few features such as wildcard type event listeners

// this will fire on user.ready, user.disconnect, user.whatever ...
myEvents.on('user.*', data => {
   console.log('something user related happened!', data)
})
Enter fullscreen mode Exit fullscreen mode

But it can be done even better. If this stuff interests you then have a look at my Emitter class which is designed specifically to fill these gaps.

Collapse
 
usamaashraf profile image
Usama Ashraf

Thanks so much for the valuable input Jochem! If you don't mind, I might include eventemitter2 and wild card-based subscriptions in this article and on another platform where I published it.

Collapse
 
jochemstoel profile image
Jochem Stoel

By all means! Sharing is caring.

Collapse
 
chenge profile image
chenge

May I ask a question, how do I fire a event?

Collapse
 
usamaashraf profile image
Usama Ashraf

Hey chenge, firing is just another word for "emitting".

Collapse
 
chenge profile image
chenge

Hi Usama thanks. But sendEmailOnRegistration doesn't work, why?

user = {};
myEmitter.emit('user-registered', user);

myEmitter.on('user-registered', (user) => {
  console.log("listened...");
});

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



Thread Thread
 
chenge profile image
chenge

I see. I must subscribe first.

Collapse
 
spirodonfl profile image
Spiro Floropoulos

This was a good read. You took a somewhat smaller concept and fleshed it out.

Nice work!

Collapse
 
usamaashraf profile image
Usama Ashraf

Thank you!

Collapse
 
_ferh97 profile image
Fernando Hernandez

Using event architecture in our APIs will make them more efficiently? Which are the advantages?

Collapse
 
chenge profile image
chenge

The project is divided handlers and listeners?