Event emitters

An EventEmitter is a pattern that allows for handling asynchronous events. It is commonly used to handle events for objects. An EventEmitter instance can emit named events that cause functions (listeners) to be called. These listeners are functions that respond to the emitted events. The handler functions are invoked with this value set to the EventEmitter object itself (except arrow functions). Any value returned by an event handler function is ignored.

Key methods of EventEmitter object:

  • on(event, listener) and its alias addListener(event, listener): Adds a listener for the specified event.
  • once(event, listener): Adds a one-time listener for the specified event. The listener is invoked only the first time the event is fired, after which it is removed.
  • emit(event, [...args]): Synchronously calls each of the listeners registered for the event in the order they were registered, passing the supplied arguments to each.
  • off(event, listener) and its alias removeListener(event, listener): Removes a specific listener for the specified event.
  • removeAllListeners([event]): Removes all listeners, or those of the specified event.
import EventEmitter from "events";

const emitter = new EventEmitter();
emitter.on("event", (arg1, arg2) => {
  console.log(`Event with arguments: ${arg1}, ${arg2}`);
});

emitter.emit("event", "value1", "value2");

To remove a listener, call the removeListener() method of the EventEmitter object and pass the event type as the first argument and the event listener as the second argument.

function onEvent() {
  console.log("This will not be logged anymore after removal");
}

emitter.on("removableEvent", onEvent);
emitter.emit("removableEvent"); // Logs the message

emitter.off("removableEvent", onEvent);
emitter.emit("removableEvent"); // Does not log anything

When an event occurs, Node invokes all handler functions synchronously in order they have been registered. Therefore, it is important to keep event handler functions non-blocking and fast. For heavy workload, consider setTimeout() or setImmediate() functions to schedule the computation asynchronously after all pending callbacks and events have been handled.

When using EventEmitter, if an error is emitted and there's no listener for the "error" event, Node will throw an uncaught exception and terminate the process. To avoid this, always add a listener for the "error" event.

emitter.on("error", (err) => {
  console.error("An error occurred:", err.message);
});
emitter.emit("error", new Error("Something went wrong"));

You should always handle the "error" event to avoid unhandled exceptions. This is because EventEmitter can emit errors as events, and if there's no listener for the error event, Node will throw an unhandled exception and terminate the process.

import fs from "fs";
const readableStream = fs.createReadStream("nonexistentfile.txt");

readableStream.on("error", (err) => {
  console.error("Stream error:", err.message);
});

readableStream.on("data", (chunk) => {
  console.log("Received chunk:", chunk);
});

If you have multiple custom events or sources of errors, it's crucial to add an error listener directly on the EventEmitter instance itself. This way, you ensure that any error emitted by the instance, regardless of the source or type of event, is caught and handled. By placing the error listener at the instance level, you centralize error handling.

const anotherEmitter = new EventEmitter();
anotherEmitter.on("error", (err) => {
  console.error("Caught an error:", err.message);
});

anotherEmitter.on("event1", () => {
  // Simulate an error
  anotherEmitter.emit("error", new Error("Error from event1"));
});

anotherEmitter.on("event2", () => {
  // Simulate another error
  anotherEmitter.emit("error", new Error("Error from event2"));
});

// Trigger the events
anotherEmitter.emit("event1");
anotherEmitter.emit("event2");