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. 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.

Basic Usage

Registering a Listener

Use .on() to register a listener function for a specific event.

Emitting an Event

Use .emit() to trigger the event.

Passing Arguments

You can pass arguments to the event listener.

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
app.js

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.

Advanced Event Patterns

One-Time Listeners

Use .once() for listeners that should only fire once.

Removing Listeners

Listener Count and Names

Prepending Listeners

Listeners are normally added to the end of the queue. Use .prependListener() to add to the beginning.

Building a Custom Event-Driven System

Let’s build a practical example: a task queue system.

Async Iterator Support

Node.js 12+ supports async iterators with events:

EventEmitter vs Other Patterns

When should you use EventEmitter versus callbacks, promises, or streams? This decision comes up often in Node.js design: 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

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.