Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
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
Here’s a simple example of creating and using the EventEmitter
class:
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
If an error event is emitted in the case of errors, it’s treated as a special case within Node.js. If the EventEmitter
doesn’t have at least one error event listener register and an error is emitted, the error is thrown, and the stack trace of the error will be printed, and the process will exit. For example, if we have the following code:
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
Node.js
will emit one special event without writing any code to emit the event: The newListener
. The newListener
event is emitted before a listener is added to the internal array of listeners. For example, if we have the following code:
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.
Top comments (2)
Great read, thanks.
Thanks for much for reading!