Event-Driven Programming in Node.js

Mirza Leka
8 min readJul 31, 2024

--

This article will explore the insights of the Events module in Node.js

Getting Started

Event Driven Programming is a paradigm in which the flow of the program is determined by events — such as user actions (mouse clicks, key presses), sensor outputs, or messages from other programs.

In the context of a Node.js application, the Events are used to distribute data between applications (microservices) or handle the Inter-Process-Communication within the same application.

Three key concepts:

  • Event Emitter (also known as the Producer / Publisher)
    An event emitter emits named events that occurred in the application.
  • Event Listener (also known as the Consumer / Subscriber)
    Event listener is a function that listens to the particular events.
  • Event Handler
    A callback function that is tasked to handling the event, triggered by the Event Listener.

Browser Events

If you’re coming from frontend background, the closest thing to an event in Node.js is an event listener in vanilla JavaScript. Often in web apps, you create listeners for various DOM events, like mouse clicks, key press or other types of events.

 <button id="submit">Submit</button>
  <script>
const submitBtn = document.getElementById('submit');

submitBtn.addEventListener('click', (event) => {
console.log('Clicked!');
});
</script>

In the example above,

  • the end user who clicks the button is the Event Emitter
  • The ‘click’ is an Event that app is Listening to
  • And the code that executes afterwards is an Event Handler

That said, JavaScript also allows for creating Custom Events that are triggered without the user interaction. More on Custom Events API.

Events in Node.js

To get started, import the events module into your Node.js app. This is a built-in module and does not require installation.

const EventEmitter = require('events');

The events module exposes the EventEmitter class that is used for creating new emitter instances.

const EventEmitter = require('events');
const emitter = new EventEmitter();

The emitter instance is then used to create (emit) events, as well as listen to events. Each event has a name (any name you desire) and the data it dispatches.

const data = 'Hello World!';
emitter.emit('EVENT-NAME', data);

The event listener listens for a specified event and acts upon receiving it.

emitter.on('EVENT-NAME', (data) => {
/* Handle data */
});

Full example

// app.js

const EventEmitter = require('events');
const emitter = new EventEmitter();

// listen to events (Event Listener)
emitter.on('greetings', (data) => {
console.log('data :>> ', data);
// (Event Handler)
});

// create an event (Event Emitter)
emitter.emit('greetings', 'Hello World!');

Running the app.js file the output should print as expected.

> node app.js
data :>> Hello World!

Event Driver vs Request Response programming

What makes this approach different than the typical request-response programming paradigm?

Loose Coupling

The event emitter and the event listener are not dependant on each other. In a typical request-response model, the two components/functions that interact with each other are tightly coupled:

// Event emitter function
function eventEmitter(data) {
console.log('Emitting data: ', data);
// Directly calling the listener function (tight coupling)
listenToData(data);
}

// Listener function
function listenToData(data) {
console.log('Data received: ', data);
// Process the data...
}

// Emitting an event
eventEmitter('Hello, world!');

Asynchronous Behavior

In the example shown we can see that there is blocking execution between the emitter and the handler. When one part of your program emits an event, the response is received in real-time.

// listen to events
emitter.on('greetings', (data) => {
console.log('data :>> ', data);
});

// create an event
emitter.emit('greetings', 'Hello World!');

One or multiple listeners

An event emitter can have one or multiple listeners. They can be called first, only once, after a certain period and they can be terminated. Each listener can peform a different action.

// multiple listeners
emitter.on('greetings', (data) => {
console.log('event1 :>> ', data);
/* do something */
});

emitter.on('greetings', (data) => {
console.log('event2 :>> ', data);
/* do something else */
});

Data Uncertainty

There are some drawbacks too. The listeners do not know about the structure or type of data they will receive, nor where the events originate, leading to potential challenges with debugging and tracking events.

Also it’s a good practice to terminate the listeners when they’re no longer needed, as they’ll remain listening indefinitely.

Order Matters

Notice that the listener was set before the event was emitted.

// listen to events
emitter.on('greetings', (data) => {
console.log('data :>> ', data);
});

// create an event
emitter.emit('greetings', 'Hello World!');

If the event was emitted before the listener was registered, there wouldn’t be any output from the listener.

// create an event
emitter.emit('greetings', 'Hello World!');

// listen to events
emitter.on('greetings', (data) => {
console.log('data :>> ', data);
});
> node app.js
< no output >

This is an example of a late listener (subscriber). This listener has missed the first event, but will capture every subsequent.

emitter.emit('greetings', '1');

emitter.on('greetings', (data) => {
console.log('data :>> ', data); // 2, 3...
});

emitter.emit('greetings', '2');
emitter.emit('greetings', '3');

You can think of an Event as a live show on TV. If you turn on the TV on time you’ll watch the show, but if you join in late, you’ll miss it. And it can’t be rewinded.

Managing Listeners

Now let’s look at a couple of techniques to manage event listeners.

Once

The once() method is a special type of event listener that will execute only the first time the event is dispatched (emitted).

const { EventEmitter } = require('events');
const greetingEmitter = new EventEmitter();

// called only once
greetingEmitter.once('greetings', message => {
console.log(`Will execute only once: ${message}`);
});

On

The on() method is a listener that will listen for events indefnitely (until terminated).

// called for every emit()
greetingEmitter.on('greetings', message => {
console.log(`Will execute for each message: ${message}`);
});

Increase the number of listeners

By default you can only have up to 10 listeners for the emitter instance:

console.log('maxListeners :>> ', greetingEmitter.getMaxListeners()); // 10

This can be overriden to support any number of listeners you desire.

greetingEmitter.setMaxListeners(100)

console.log('maxListeners :>> ', greetingEmitter.getMaxListeners()); // 100

Setting the order of precedence for events:

You can also override the events listener, decide what event listener will be triggered before the other.

// Triggered multiple times, but always first
greetingEmitter.prependListener('event', callback)

// Triggered once and before other 'once' events
greetingEmitter.prependOnceListener('event', callback)

The order of operations matters here as well.

Remove Listener

Removing unused listeners is important for avoiding memory leaks. To remove one listener for the particular event, call a removeListener() method on the emitter and pass the event name and the event handler you wish to remove:

emitter.removeListener('EVENT-NMAE', listenerYouWantToRemove);

// e.g.
greetingEmitter.removeListener('greetings', greetingsHandler);

Remove all Listeners

To remove all the listeners on the event simply call removeAllListeners() method.

greetingEmitter.removeAllListeners('greetings');

Examples with Custom Events

So far we’ve seen events been in the same file. The beautiful part about the events is that they can be used to transmit data between files.

How do events know about each other?
The rule of thumb is that the Events must be in the same process.

Start by creating the main file that will register the event emitters and receivers:

// main.js

// entry point for running the events
require('./parent');
require('./child1');
require('./child2');

In child1.js file we’ll will receive an event from the parent and immediately terminate the handler:

// child1.js

const { greetingEmitter } = require('./parent');

function greetingsHandler(message) {
console.log(`[1ST Child] Message from parent: ${message}`);

// remove listener
greetingEmitter.removeListener('greetings', greetingsHandler);
console.log(`[1ST Child] handler is terminated.`);
}

// called upon receiving an event
greetingEmitter.on('greetings', greetingsHandler);

In child2.js we’ll subscribe to the same event twice. First listener will be triggered only once, while the second will remain active during the lifetime of the application:

// child2.js
const { greetingEmitter } = require('./parent');

greetingEmitter.once('greetings', message => {
console.log(`[2ND Child] Will execute once: ${message}`);
});

greetingEmitter.on('greetings', message => {
console.log(`[2ND Child] Message from parent: ${message}`);
});

Finally, in the parent.js file we’ll create an event emitter instance (that will act as a singleton), export it and then emit multiple messages after the timeout of 1000 ms.

// parent.js

const { EventEmitter } = require('events');

// export the emitter used in the child1.js and child2.js
const greetingEmitter = new EventEmitter();
module.exports = { greetingEmitter };

function sendGreeting() {
greetingEmitter.emit('greetings', 'Hello World!');
console.log('[Parent] sent message!')
};

setTimeout(() => {
sendGreeting();
sendGreeting();
sendGreeting();
}, 1000)

Now let’s run the main.js file and verify the result:

$ node main.js
...

[1ST Child] Message from parent: Hello World!
[1ST Child] handler is terminated.
[2ND Child] Will execute once: Hello World!
[2ND Child] Message from parent: Hello World!
[Parent] sent message!
[2ND Child] Message from parent: Hello World!
[Parent] sent message!
[2ND Child] Message from parent: Hello World!
[Parent] sent message!

More examples of using the events module:

Events in existing Node.js APIs

Node.js events to handle inter-process communication, like notifying processes about specific events or requesting certain behaviors.

Process

The process global object makes use of the events to listen for changes in the application, such as:

// SIGNAL INTERRUPTED (When you hit CTRL+C)
process.on('SIGINT', () => {
console.log('App is shutting down!');
});

// BEFORE EVENT OCCURS
process.on('beforeExit', () => {
// Close database, queue connections
});

// WHEN ERROR IS THROWN BUT NOT HANDLED
process.on('uncaughtException', (error) => {
console.error(error);
process.exit(1); // Terminate the active process with signal 1
});

HTTP Server

The module responsible for REST communication between services utilizes the events under the hood. For example, this is standard GET request via HTTP module in Node.js:

const https = require('https'); // native module. Do not need to install

https.get('https://jsonplaceholder.typicode.com/todos/1', (res) => {

console.log('res.statusCode :>> ', res.statusCode); // 200
let responseBody;

// listen for the 'data' built-in event. Data is arriving in chunks
res.on('data', (incomingData) => {
responseBody += incomingData;
});

// process the data once it completely arrives
res.on('end', () => {
console.log('responseBody :>> ', responseBody); // {...}
})
});

Learn more about the HTTP module:

TCP Server

The NET module provides an asynchronous network API for creating stream-based TCP or IPC servers and clients. It also utilizes events to start and stop connections.

const server = require('net').createServer();

// server listening for clients to connect
server.on('connection', socket => {

socket.write('Welcome new client!\n');

// socket listening on incoming 'data' built-in event
socket.on('data', data => {

socket.write(data);
});


// Closing connection event
socket.on('end', () => {
console.log('Client disconnected!');
});

});


server.listen(3000, () => console.log('Server started on port 3000'));

File System

It’s a common practice to utilize the events with file system streams. Here is an example of error handling when writing to a file using events:

const fs = require('fs');


// write to file
fs.createWriteStream('File-name.txt', {flags: 'a'})
.end('File Content, e.g. Lorem Ipsum')

// listening to error event
.on('error', () => console.log('Oh noes!'));

And similar APIs.

Wrapping up

Node.js makes use of the events module and the event-driven architecture to perform asynchronous tasks. There is a long list of bult-in events that occur under the hood. Node.js also lets you create custom ones.

For more on Node.js, be sure to check out my articles below:

Node.js Starter

8 stories

Express.js Starter (Express-APIvantage)

4 stories

Bye for now 👋

--

--

Mirza Leka

Web Developer. DevOps Enthusiast. I share my experience with the rest of the world. Follow me on https://twitter.com/mirzaleka for news & updates #FreePalestine