Skip to main content
Node.js Event Loop

Events & EventEmitter

Node.js is built around an event-driven architecture. This means that certain objects (called “emitters”) emit named events that cause Function objects (“listeners”) to be called. For example, a net.Server object emits an event each time a peer connects to it; an fs.ReadStream emits an event when the file is opened; a stream emits an event whenever data is available to be read. All objects that emit events are instances of the EventEmitter class.

The events Module

To use events, we need the events module.
const EventEmitter = require('events');
const myEmitter = new EventEmitter();

Basic Usage

Registering a Listener

Use .on() to register a listener function for a specific event.
myEmitter.on('event', () => {
  console.log('An event occurred!');
});

Emitting an Event

Use .emit() to trigger the event.
myEmitter.emit('event');
// Output: An event occurred!

Passing Arguments

You can pass arguments to the event listener.
myEmitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

myEmitter.emit('greet', 'Alice');
// Output: Hello, Alice!

Extending EventEmitter

In real-world applications, you usually extend the EventEmitter class to create your own modules that emit events. Let’s create a Logger class that emits a ‘message’ event whenever a message is logged. logger.js
const EventEmitter = require('events');
const uuid = require('uuid'); // Hypothetical dependency for IDs

class Logger extends EventEmitter {
  log(msg) {
    // Call event
    this.emit('message', { id: Date.now(), msg });
    console.log(msg);
  }
}

module.exports = Logger;
app.js
const Logger = require('./logger');
const logger = new Logger();

// Register listener
logger.on('message', (data) => {
  console.log('Called Listener:', data);
});

logger.log('Hello World');
logger.log('Hi there');

Handling Errors

When an error occurs within an EventEmitter instance, the typical action is for an ‘error’ event to be emitted. 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.
myEmitter.on('error', (err) => {
  console.error('Whoops! There was an error');
});

myEmitter.emit('error', new Error('Something went wrong!'));

Advanced Event Patterns

One-Time Listeners

Use .once() for listeners that should only fire once.
myEmitter.once('connect', () => {
  console.log('Connected! This will only log once.');
});

myEmitter.emit('connect'); // Logs message
myEmitter.emit('connect'); // Nothing happens

Removing Listeners

const greet = () => console.log('Hello!');

myEmitter.on('greet', greet);
myEmitter.emit('greet'); // Hello!

myEmitter.removeListener('greet', greet);
// OR: myEmitter.off('greet', greet);
myEmitter.emit('greet'); // Nothing happens

// Remove all listeners for an event
myEmitter.removeAllListeners('greet');

// Remove all listeners for all events
myEmitter.removeAllListeners();

Listener Count and Names

myEmitter.on('data', () => {});
myEmitter.on('data', () => {});
myEmitter.on('error', () => {});

console.log(myEmitter.listenerCount('data')); // 2
console.log(myEmitter.eventNames()); // ['data', 'error']

Prepending Listeners

Listeners are normally added to the end of the queue. Use .prependListener() to add to the beginning.
myEmitter.on('event', () => console.log('First added'));
myEmitter.prependListener('event', () => console.log('Added first, runs first'));
myEmitter.emit('event');
// Output:
// Added first, runs first
// First added

Building a Custom Event-Driven System

Let’s build a practical example: a task queue system.
const EventEmitter = require('events');

class TaskQueue extends EventEmitter {
  constructor(concurrency = 1) {
    super();
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  push(task) {
    this.queue.push(task);
    this.emit('taskAdded', { queueLength: this.queue.length });
    this.process();
  }

  async process() {
    while (this.running < this.concurrency && this.queue.length > 0) {
      const task = this.queue.shift();
      this.running++;
      
      this.emit('taskStarted', { task: task.name });
      
      try {
        const result = await task.execute();
        this.emit('taskCompleted', { task: task.name, result });
      } catch (error) {
        this.emit('taskFailed', { task: task.name, error });
      } finally {
        this.running--;
        this.process(); // Process next task
      }
    }

    if (this.running === 0 && this.queue.length === 0) {
      this.emit('idle');
    }
  }
}

// Usage
const queue = new TaskQueue(2); // 2 concurrent tasks

queue.on('taskAdded', (data) => console.log('Task added. Queue length:', data.queueLength));
queue.on('taskStarted', (data) => console.log('Started:', data.task));
queue.on('taskCompleted', (data) => console.log('Completed:', data.task));
queue.on('taskFailed', (data) => console.error('Failed:', data.task, data.error));
queue.on('idle', () => console.log('All tasks completed!'));

queue.push({
  name: 'Task 1',
  execute: () => new Promise(resolve => setTimeout(() => resolve('Done'), 1000))
});

queue.push({
  name: 'Task 2', 
  execute: () => Promise.resolve('Quick task')
});

Async Iterator Support

Node.js 12+ supports async iterators with events:
const { on } = require('events');

async function processEvents(emitter) {
  for await (const [data] of on(emitter, 'data')) {
    console.log('Received:', data);
    if (data === 'close') break;
  }
  console.log('Done processing events');
}

// Usage
const emitter = new EventEmitter();
processEvents(emitter);

emitter.emit('data', 'first');
emitter.emit('data', 'second');
emitter.emit('data', 'close');

Best Practices

  1. Always handle errors: An unhandled ‘error’ event crashes the process
  2. Remove listeners when done: Prevent memory leaks in long-running apps
  3. Use once() for one-time events: Connection, initialization
  4. Set max listeners when needed: emitter.setMaxListeners(20)
  5. Use named functions: Easier to remove than anonymous functions

Summary

  • EventEmitter is the foundation of Node.js async patterns
  • Use .on() to register listeners, .emit() to trigger events
  • Always handle ‘error’ events to prevent crashes
  • Use .once() for one-time listeners
  • Extend EventEmitter to create event-driven classes
  • Clean up listeners with .off() or .removeListener() to prevent memory leaks
myEmitter.emit(‘error’, new Error(‘Something went wrong’));

## Once

If you want a listener to be called only once, use `.once()`.

```javascript
myEmitter.once('log', () => console.log('Log once'));

myEmitter.emit('log'); // Prints "Log once"
myEmitter.emit('log'); // Ignored

Summary

  • Event-Driven Architecture is core to Node.js.
  • The EventEmitter class is used to bind events and listeners.
  • Use .on() to listen for events.
  • Use .emit() to trigger events.
  • You can extend EventEmitter to build custom classes that emit events.
  • Always handle the 'error' event to prevent crashes.