Using Events in Node.js (Part 1)

How to emit and listen to events

John Au-Yeung
Dec 4, 2019 · 8 min read
Source: Wikipedia Commons

The core feature of Node.js is asynchronous programming. This means that code in Node.js may not be executed sequentially. Therefore, data may not be determined in a fixed amount of time. This means that to get all the data we need, we have to pass data around the app when the data is obtained. This can be done by emitting, listening to, and handling events in a Node.js app. When an event with a given name is emitted, the event can listen to the listener, if the listener is specified to listen to the event with the name. Event emitter functions are called synchronously. The event listener code is a callback function that takes a parameter for the data and handles it. Node.js has an EventEmitter class — it can be extended by a new class created to emit events that can be listened to by event listeners.


Define Event Emitters

const EventEmitter = require('events');class Emitter extends EventEmitter {}const eventEmitter = new Emitter();
eventEmitter.on('event', () => {
console.log('event emitted!');
});
eventEmitter.emit('event');

We should get ‘event emitted!’ in the console log. In the code above, we created the Emitter which extends the EventEmitter class, which has the emit function we called in the last line. The argument of the emit function is the name of the event, which we listen to in this block of code:

eventEmitter.on('event', () => {
console.log('event emitted!');
});

The callback function, after the 'event' argument above, is the event handler function, that runs when the event is received.

In the code above, we emitted an event. However, it’s not very useful since we didn’t pass any data with the emitted event when we emit the event so it doesn’t do much. Therefore, we want to send data with the event so that we can pass data around so that we can do something useful in the event listener. To pass data when we emit an event, we can pass in extra arguments after the first argument, which is the event name. For instance, we can write the following code:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('event', (a, b) => {
console.log(a, b);
});
eventEmitter.emit('event', 'a', 'b');

If we run the code above, we get ‘a’ and ‘b’ in the console.log statement insider the event handler callback function. As we see, we can pass in multiple arguments with the emit function to pass data into event handlers that subscribe to the event. After the first one, the arguments in the emit function call are all passed into the event listener’s callback function as parameters, so they can be accessed within the event listener callback function.

We can also access the event emitter object inside the event listener callback function. All we have to do is change the arrow function of the callback to a tradition function, as in this code:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('event', function(a, b){
console.log(a, b);
console.log(`Instance of EventEmitter: ${this instanceof EventEmitter}`);
console.log(`Instance of Emitter: ${this instanceof Emitter}`);
});
eventEmitter.emit('event', 'a', 'b');

If we run the code above, we get this logged in the console.log statements inside the event listener callback function:

a b
Instance of EventEmitter: true
Instance of Emitter: true

On the other hand, if we have the following:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('event', (a, b) => {
console.log(a, b);
console.log(`Instance of EventEmitter: ${this instanceof EventEmitter}`);
console.log(`Instance of Emitter: ${this instanceof Emitter}`);
});
eventEmitter.emit('event', 'a', 'b');

Then we get this logged in the console.log statements inside the event listener callback function.:

a b
Instance of EventEmitter: false
Instance of Emitter: false

This is because arrow functions do not change the this object inside it. However, tradition functions do change the content of the this object.

EventEmitter calls all listeners synchronously, in the order that they’re registered. This eliminates the chance of race conditions and other logic errors. To handle events asynchronously, we can use the setImmediate() or the process.nextTick() methods:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('event', (a, b) => {
setImmediate(() => {
console.log('event handled asychronously');
});
});
eventEmitter.emit('event', 'a', 'b');

In the code above, we put the console.log inside a callback function of the setImmediate function, which will run the event handling code asynchronously instead of synchronously.

Events are handled every time they’re emitted. For example, if we have:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
let x = 1;
eventEmitter.on('event', (a, b) => {
console.log(x++);
});
for (let i = 0; i < 5; i++){
eventEmitter.emit('event');
}

Since we emitted the ‘event’ event five times, we get this:

1
2
3
4
5

If we want to emit an event and handle it only the first time it’s emitted, then we use the eventEmitter.once() function, as in this code:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
let x = 1;
eventEmitter.once('event', (a, b) => {
console.log(x++);
});
for (let i = 0; i < 5; i++){
eventEmitter.emit('event');
}

As expected, we only get this logged in the console.log statement of the event handler above:

1

Error Handling

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.emit('error', new Error('Error occured'));

Then we get something like this and the program exits:

Error [ERR_UNHANDLED_ERROR]: Unhandled error. (Error: Error occured
at evalmachine.<anonymous>:5:28
at Script.runInContext (vm.js:133:20)
at Object.runInContext (vm.js:311:6)
at evaluate (/run_dir/repl.js:133:14)
at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
at ReadStream.emit (events.js:198:13)
at addChunk (_stream_readable.js:288:12)
at readableAddChunk (_stream_readable.js:269:11)
at ReadStream.Readable.push (_stream_readable.js:224:10)
at lazyFs.read (internal/fs/streams.js:181:12))
at Emitter.emit (events.js:187:17)
at evalmachine.<anonymous>:5:14
at Script.runInContext (vm.js:133:20)
at Object.runInContext (vm.js:311:6)
at evaluate (/run_dir/repl.js:133:14)
at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
at ReadStream.emit (events.js:198:13)
at addChunk (_stream_readable.js:288:12)
at readableAddChunk (_stream_readable.js:269:11)
at ReadStream.Readable.push (_stream_readable.js:224:10)

To prevent the Node.js program from crashing, we can listen to the error event with a new event listener and handle the error gracefully in the error event handler. For example, we can write:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('error', (error) => {
console.log('Error occurred');
});
eventEmitter.emit('error', new Error('Error occurred'));

Then we get “error occurred” logged. We can also get the error content with the error parameter of the event handler callback function. If we log it, as in this code:

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('error', (error) => {
console.log(error);
});
eventEmitter.emit('error', new Error('Error occurred'));

We will get something like this:

Error: Error occurred
at evalmachine.<anonymous>:7:28
at Script.runInContext (vm.js:133:20)
at Object.runInContext (vm.js:311:6)
at evaluate (/run_dir/repl.js:133:14)
at ReadStream.<anonymous> (/run_dir/repl.js:116:5)
at ReadStream.emit (events.js:198:13)
at addChunk (_stream_readable.js:288:12)
at readableAddChunk (_stream_readable.js:269:11)
at ReadStream.Readable.push (_stream_readable.js:224:10)
at lazyFs.read (internal/fs/streams.js:181:12)

More Ways to Deal with Events

const EventEmitter = require('events');
class Emitter extends EventEmitter {}
const eventEmitter = new Emitter();
eventEmitter.on('newListener', (event, listener) => {
console.log(event);
});

Then we get something like this logged:

Emitter {
_events: [Object: null prototype] { newListener: [Function] },
_eventsCount: 1,
_maxListeners: undefined }

This happens even when no events are emitted. Whatever is in the handler will be run before the code in event handlers for any other events.

The removeListener function can be used to stop event listener functions from listening to events. This takes two arguments: The first is a string that represents the event name, the second is the function that you want to stop using to listen to events. For example, if we want to stop listening to the “event” event with our listener function, then we can write this:

const EventEmitter = require('events');
class Emitter extends EventEmitter { }
const eventEmitter = new Emitter();
const listener = () => {
console.log('listening');
}
eventEmitter.on('event', listener)setInterval(() => {
eventEmitter.emit('event');
}, 300);
setTimeout(() => {
console.log("removing");
eventEmitter.removeListener('event', listener);
}, 2000);

Then we get something like this in the output:

Timeout {
_called: false,
_idleTimeout: 2000,
_idlePrev: [TimersList],
_idleNext: [TimersList],
_idleStart: 1341,
_onTimeout: [Function],
_timerArgs: undefined,
_repeat: null,
_destroyed: false,
[Symbol(unrefed)]: false,
[Symbol(asyncId)]: 10,
[Symbol(triggerId)]: 7 }listening
listening
listening
listening
listening
listening
removing

The event emitter emits the “event” event in the code above once every 300 milliseconds. This is listened to by the listener function, until it’s been prevented from listening again by calling the removeListener function with the “event” as the event name the listener event listener function in the callback of the setTimeout function.

Multiple event listeners can register for a single event. By default, the limit for the maximum number of event listeners is ten. We can change this with the defaultMxListeners function in the EventEmitter class. We can set it to any positive number. If it’s not a positive number, then a TypeError is thrown. If more listeners than the limit are registered then a warning will be output. For example, if we run the following code to register 11 event listeners for the “event” event:

const EventEmitter = require('events');
class Emitter extends EventEmitter { }
const eventEmitter = new Emitter();
const listener = () => {
console.log('listening');
}
for (i = 1; i <= 11; i++){
eventEmitter.on('event', listener);
}
eventEmitter.emit('event');

When we run the code above, we get this:

listening
listening
listening
listening
listening
listening
listening
listening
listening
listening
listening
(node:345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 event listeners added. Use emitter.setMaxListeners() to increase limit

However, if we call setMaxListeners to set it to getMaxListeners() + 1, which is 11 listeners, as seen in the following code:

const EventEmitter = require('events');
class Emitter extends EventEmitter { }
const eventEmitter = new Emitter();
eventEmitter.setMaxListeners(eventEmitter.getMaxListeners() + 1);
const listener = () => {
console.log('listening');
}
for (i = 1; i <= 11; i++){
eventEmitter.on('event', listener);
}
eventEmitter.emit('event');

Then we get the following logged:

listening
listening
listening
listening
listening
listening
listening
listening
listening
listening
listening

An important feature of Node.js is asynchronous programming. This means that code in Node.js may not be executed sequentially. Data may not be determined in a fixed amount of time. This means that to get all the data we need, we have to pass data around the app when the data obtained. We can emit events and handle them within a Node.js app.

When an event with a given name is emitted, the event can listen to the listener, if the listener is specified to listen to the event with the name. Event emitter functions are called synchronously. The event listener code is a callback function that takes a parameter for the data and handles it. Node.js has an EventEmitter class that can be extended by a new class that we create to emit events that can be listened to by event listeners. With the EventEmitter class, we can create new EventEmitter classes that can emit and listen to events. We can attach multiple event listeners to one event that can do different things. Also, we can set the maximum number of event listeners to as many as we want. Finally, we can choose to handle events asynchronously instead of synchronously.

Better Programming

Advice for programmers.

John Au-Yeung

Written by

Web developer. Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade