Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. The Fire Alarm Analogy: Think of EventEmitter like a building’s fire alarm system. The smoke detector (emitter) does not know which people (listeners) are in the building or what they will do when the alarm sounds. Some will grab their laptops, some will call 911, some will head for the exits. The detector’s only job is to broadcast the event. This decoupling—the emitter does not need to know about the listeners—is what makes event-driven architecture so powerful and flexible. 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. This is one of the most important patterns in Node.js: an unhandled ‘error’ event will crash your entire process. In production, this means your server goes down for all users because of a single unhandled error in one EventEmitter instance. Always register an ‘error’ listener.
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');

EventEmitter vs Other Patterns

When should you use EventEmitter versus callbacks, promises, or streams? This decision comes up often in Node.js design:
PatternCommunicationCouplingBest for
CallbackOne caller, one handlerTight — caller must know the handlerSingle async operation with one result
Promise / async-awaitOne caller, one resultModerate — chain of .then() or awaitSequential async operations
EventEmitterOne emitter, many listenersLoose — emitter does not know listenersMultiple consumers, ongoing events, plugin systems
StreamOne producer, one consumer (piped)Moderate — connected via pipeOrdered data flow with backpressure
Decision framework:
  • If a thing happens once and you need the result: use a Promise.
  • If a thing happens multiple times and you need to notify one consumer: use a Stream.
  • If a thing happens multiple times and you need to notify many consumers who do different things: use EventEmitter.
  • If you are building a plugin system where external code should react to internal events without modifying the core: EventEmitter is the natural fit.
Edge case — mixing EventEmitter with Promises: A common mistake is emitting an event inside a Promise chain and expecting the listener to run synchronously within that chain. Listeners registered with .on() are synchronous — they run immediately and in order when .emit() is called. But if a listener throws, it throws synchronously in the .emit() call, which can break your Promise chain in unexpected ways. Wrap .emit() in a try/catch if your listeners are not fully under your control.

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
Memory leak pitfall: If you register listeners inside a request handler or a loop without removing them, you will leak memory. Node.js warns you when an emitter exceeds 10 listeners (the default max) by printing MaxListenersExceededWarning. Do not silence this warning by blindly calling setMaxListeners(Infinity). Instead, investigate why listeners are accumulating—it almost always indicates a bug where listeners are being added repeatedly without being cleaned up. In long-running servers, this is one of the most common causes of gradual memory growth that eventually crashes the process.

Summary

  • EventEmitter is the foundation of Node.js async patterns—streams, HTTP servers, and most core APIs are built on it
  • Use .on() to register listeners, .emit() to trigger events
  • Always handle ‘error’ events to prevent crashes—this is non-negotiable in production
  • Use .once() for one-time events like connection establishment or initialization
  • Extend EventEmitter to create your own event-driven classes
  • Clean up listeners with .off() or .removeListener() to prevent memory leaks in long-running processes
  • Watch for MaxListenersExceededWarning as an early signal of memory leaks

EventEmitter Performance Characteristics

OperationPerformanceNotes
emit() with 0 listeners~50nsNearly free — no work to do
emit() with 1 listener~100nsDirect function call
emit() with 10 listeners~500nsLinear in listener count
emit() with 100 listeners~5usStill fast, but consider if this is a design smell
on() / addListener()O(1)Pushes to internal array
removeListener()O(n)Searches array by reference; use named functions to make this practical
Edge case — listener ordering guarantees: Listeners fire in the order they were registered. This is a guarantee, not an implementation detail. Some codebases rely on this ordering for middleware-like patterns. However, if you use prependListener(), that listener jumps to the front. Mixing on() and prependListener() on the same event across multiple modules can create execution orders that are difficult to reason about. If ordering matters, document it explicitly or use a single orchestrating listener.