Events

Node.js is perfect for event-driven applications. Every action on a computer is an event. Like when a connection is made or a file is opened. Objects in Node.js can fire events, like the readStream object fires events when opening and closing a file:

var fs = require('fs');
var rs = fs.createReadStream('./demofile.txt'); // (*)
rs.on('open', function () {
  console.log('The file is open');
});

Note that the event open is predefined for fs object. Whenever the line (*) runs, it emits an event called open. As we have defined a listener for that event, it runs whenever open events occur.

All objects that emit events are instances of the EventEmitter class. These objects expose an eventEmitter.on() function that allows one or more functions to be attached to named events emitted by the object. Typically, event names are camel-cased strings but any valid JavaScript property key can be used.

When the EventEmitter object emits an event, all of the functions attached to that specific event are called synchronously. Any values returned by the called listeners are ignored and will be discarded.

The following example shows a simple EventEmitter instance with a single listener. The eventEmitter.on() method is used to register listeners, while the eventEmitter.emit() method is used to trigger the event.

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});
myEmitter.emit('event');

Custom Event

You can create your own custom event using the class EventEmitter. To use EventEmitter, you need to require core module events -

const EventEmitter = require('events')

Event emitters is a core module for Node developers to implement the observer pattern. The observer pattern has the following: an observer, an event and an event emitter.

The flow goes like this:

Consider this simple observer pattern code which creates a Job class and then instantiates it. Later on, the object created from the class has an observer/event listener attached (job.on('done'...)) and an event is emitted/triggered.

Consider the following example -

const EventEmitter = require('events')

class Job extends EventEmitter {}
job = new Job()

job.on('done', function(timeDone){
  console.log('Job was pronounced done at', timeDone)
})

job.emit('done', new Date())
job.removeAllListeners()  // remove  all observers

// Output --
Job was pronounced done at <time>

Note that, the emit method, it takes event name as the first argument. And rest of the arguments are passed to the handler function of the on method.

let EventEmitter = require('events');
class Scream extends EventEmitter{};
scream = new Scream();
scream.on('screaming', function(name, time){
  console.log(`${name} is sreaming on ${time}`);
});
scream.emit('screaming', "Santanu Bera", new Date());

Here, the first argument "screaming" is used for the name of the event. And remaining two arguments are passed to the handler function as name and time respectively.

Value of this

The value of this refers to the object on which the listener is attached to.

let EventEmmiter = require("events");
class Student extends EventEmmiter{}
let student = new Student();
student.on("admission", function(name){
  console.log("A New Student " + name + " has been enrolled !");
  console.log(this);
})
student.emit("admission", "Santanu Bera");

// Output : 
A New Student Santanu Bera has been enrolled !
Student {
  domain: null,
  _events: { admission: [Function] },
  _eventsCount: 1,
  _maxListeners: undefined }

But note that, if you use arrow function instead, this won't refer to the Student object anymore. Instead this will refer to the object in the outer scope.

student.on("admission", (name) => {
  console.log("A New Student " + name + " has been enrolled !");
  console.log(this);
})

// Output:
A New Student Santanu Bera has been enrolled !
{}

Executing Observer Code Only once

Events can be triggered/emitted multiple times. For example, in knock-knock.js the knock event is emitted multiple times.

// knock-knock.js
const EventEmitter = require('events')

class Emitter extends EventEmitter {}
emitter = new Emitter()

emitter.on('knock', function() {
  console.log('Who\'s there?')
})

emitter.on('knock', function() {
  console.log('Go away!')
})

emitter.emit('knock')
emitter.emit('knock')


// Output --
Who's there?
Go away!
Who's there?
Go away!

emitter.once(eventName, eventHandler) will execute observer code just once, no matter how many time this particular event was triggered.

// knock-knock-once.js
const EventEmitter = require('events')

class Emitter extends EventEmitter {}
emitter = new Emitter()

emitter.once('knock', function() {
  console.log('Who\'s there?')
})


emitter.emit('knock')
emitter.emit('knock')

// Output --
Who's there?

event.removeAllListeners()

The removeAllListeners() is used to remove all the listeners that has been assigned to the object.

let EventEmitter = require('events');
class Scream extends EventEmitter{};
scream = new Scream();
scream.on('screaming', function(name, time){
	console.log(`${name} is sreaming on ${time}`);
});
scream.emit('screaming', "Santanu Bera", new Date());
scream.removeAllListeners();
scream.emit('screaming', "Priyanka Bera", new Date());

// Output --
Santanu Bera is sreaming on Fri Feb 01 2019 17:02:19 GMT+0530 (India Standard Time)

As we have remove all the listener attached to the scream object, the last line won't print anything.

Modular Events

The observer pattern is often used to modularize code. A typical usage is to separate the event emitter class definition and the event emission into its own module but allow the observers to be defined in the main program. This allows us to customize the module behavior without changing the module code itself.

// Job.js
const EventEmitter = require('events')
class Job extends EventEmitter {
  constructor(ops) {
    super(ops)
    this.on('start', ()=>{
      this.process()
    })
  }
  process() {
     setTimeout(()=>{
      // Emulate the delay of the job - async!
      this.emit('done', { completedOn: new Date() })
    }, 700)
  }
}
module.exports = Job
// Weekly.js
var Job = require('./job.js')
var job = new Job()

job.on('done', function(details){
  console.log('Weekly email job was completed at',
    details.completedOn)
})

job.emit('start')

When you run weekly.js, the custom logic pertaining to the done event will be executed from weekly.js. This way the creators of the job.js module can keep the module flexible. They don't have to hard code any logic for the done event in the module. Consumers of the module job.js, people who write weekly.js, have the power to customize the behavior for the done event, and not only for that event. Event emitters can have multiple events: in the middle, at the start, in the end, etc. They can be called (emitted or triggered) multiple times and with data (passed as the second argument to emit() as can be seen in job.js). Furthermore, there are methods to list or remove event listeners (observers) or to specify the execution of an event listener (observer) just once (.once() method).

Error Events

When an error occurs within an EventEmmiter object, the error event is emitted. These are treated as special cases within Node.js.

If an EventEmitter does not have at least one listener registered for the 'error' event, and an 'error' event is emitted, the error is thrown, a stack trace is printed, and the Node.js process exits.

const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
// Throws and crashes Node.js

So at least one listener for error event should be attached to the EventEmmiter object.

const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
  console.error('whoops! there was an error');
});
myEmitter.emit('error', new Error('whoops!'));
// Prints: whoops! there was an error