We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through JavaScript Works average a 15% increase in salary 🚀

Blog hero image

Using Events In Node.js The Right Way

Usama Ashraf 8 November, 2018 | 4 min read

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.

BLOG.jpeg

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;

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

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

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);
// send_email_on_registration.js

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

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

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

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

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.

Originally published on dev.to

Author's avatar
Usama Ashraf
I'm a multilingual, senior software engineer who loves everything on the back-end, databases, infrastructure, containers, DevOps, the cloud and some on the front-end: e.g. React, Redux, Vue. I'm obsessed with creating scalable, fault-tolerant yet flexible services with impeccable code quality. In parallel, I've been engrossed in microservices, infrastructure automation, Progressive Web Apps & AWS architecture and continue to get better at them everyday. Currently hacking on Scala, Go, Kafka and Thrift as well. Aspiring to be a Deep Learning expert. I like to share what I know here: https://dev.to/usamaashraf https://medium.com/@usamaashraf And have some humble open-source contributions as well: https://www.npmjs.com/~usama.ashraf https://github.com/UsamaAshraf Find resort in comedy. Occasional swimmer.
    JavaScript
    Python
    Node
    React
    Vue
    Rails
    Laravel
    Go

Related Jobs

Related Issues

viebel / klipse-clj
viebel / klipse-clj
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
  • $100
viebel / klipse
  • 1
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
  • $80
viebel / klipse
  • Open
  • 0
  • 0
  • Advanced
  • Clojure
  • $80
viebel / klipse
  • Started
  • 0
  • 2
  • Advanced
  • Clojure
  • $180
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
  • $80

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through JavaScript Works average a 15% increase in salary.

Start with GithubStart with Stack OverflowStart with Email