Skip to main content

What is eventemitter in Node.js?

EventEmitter is a Node.js class from the events module that lets objects emit named events and register listener functions to handle them.

Theory

TL;DR

  • Think of EventEmitter like a dinner bell in a restaurant kitchen: ring it with a name ("order ready"), and only the listeners subscribed to that name react.
  • on() adds a persistent listener; once() adds one that removes itself after the first fire.
  • emit() is synchronous - it calls all listeners in registration order before returning.
  • Always listen for the 'error' event, or Node.js will throw and crash the process.
  • Use EventEmitter for in-process pub-sub; switch to Redis or Kafka for distributed systems.

Quick example

js
const EventEmitter = require('events'); const emitter = new EventEmitter(); // Persistent listener - fires every time emitter.on('order', (dish) => console.log(`Serving ${dish}`)); // One-time listener - auto-removed after first call emitter.once('alert', () => console.log('Evacuation!')); emitter.emit('order', 'pizza'); // → Serving pizza emitter.emit('alert'); // → Evacuation! emitter.emit('alert'); // → (nothing - already fired)

on() keeps the listener alive across multiple emits. once() removes itself automatically after the first call.

How it works internally

EventEmitter stores listeners in an internal _events object - a map where each key is an event name and the value is an array of functions. When you call emit('order', 'pizza'), Node grabs that array and calls each function synchronously, in registration order, passing 'pizza' as the argument.

No async queueing happens here. The call stack does not return until every listener finishes. So if a listener does something slow - like a blocking loop - it holds up everything that follows.

When to use

  • User login needs to trigger both an email and an analytics event from one place: EventEmitter.
  • You need a signal that fires only once (connection established, file opened): once().
  • Working with Node.js streams: they already extend EventEmitter, so 'data', 'end', and 'error' come built in.
  • Events need to cross process or server boundaries: skip EventEmitter and use a message queue like BullMQ or Redis Pub/Sub.

Custom class pattern

The most common production pattern is extending EventEmitter directly:

js
const EventEmitter = require('events'); class Database extends EventEmitter { connect() { setTimeout(() => { this.emit('connected', { host: 'localhost' }); }, 1000); } query(sql) { setTimeout(() => { this.emit('data', [{ id: 1, name: 'Alice' }]); }, 500); } } const db = new Database(); db.on('connected', ({ host }) => { console.log(`Connected to ${host}`); db.query('SELECT * FROM users'); }); db.on('data', (rows) => { console.log('Rows:', rows); }); db.connect();

The Database class stays focused on data logic. Callers decide what to do with each event. That separation is the point.

Common mistakes

1. Missing the 'error' listener

js
const ee = new EventEmitter(); ee.emit('error', new Error('boom')); // Uncaught exception - process exits

Node.js treats 'error' as special. Emit it with no listener and the process crashes. Always add one:

js
ee.on('error', (err) => console.error('Handled:', err.message));

2. Assuming emit is async

js
ee.on('heavy', () => { for (let i = 0; i < 1e8; i++) {} // blocking loop }); ee.emit('heavy'); ee.emit('light'); // delayed until 'heavy' listener finishes

emit is synchronous. If you need to offload slow work, use process.nextTick() or a worker thread inside the listener.

3. Removing the wrong listener

js
ee.on('greet', () => console.log('hi')); ee.off('greet', () => console.log('hi')); // Listener still active! Different reference.

Arrow functions create a new reference every time. Store the function first:

js
const handler = () => console.log('hi'); ee.on('greet', handler); ee.off('greet', handler); // Works correctly

4. Hitting the max listeners warning

Node.js warns when more than 10 listeners are added to a single event - this catches accidental leaks in loops. If you genuinely need more, set the limit explicitly:

js
emitter.setMaxListeners(20); emitter.getMaxListeners(); // → 20

Real-world usage

  • Node.js streams (fs.ReadStream, net.Socket) extend EventEmitter and emit 'data', 'end', 'error'.
  • http.Server emits 'request' for each incoming HTTP connection.
  • Socket.io uses EventEmitter as the base for its client-server event model.
  • Webpack's compiler emits 'done' and 'invalid' to power hot module replacement.
  • The process object itself is an EventEmitter: 'exit', 'uncaughtException', 'SIGTERM'.

In practice, the 'error' listener is the thing most teams skip in early prototypes, and it always comes back to bite them in staging.

Follow-up questions

Q: What is the difference between on and once?
A: on adds a persistent listener that fires every time the event is emitted. once wraps the listener, removes it after the first call, then invokes the original. Use once for acknowledgments or one-shot setup steps.

Q: Is emit synchronous or asynchronous?
A: Synchronous. It calls all listeners before returning. If you call emit inside a setTimeout, the emit itself is still sync within that callback.

Q: What happens if a listener throws an error?
A: The error propagates up the call stack. If an 'error' listener exists and the throwing event is 'error', it catches there. Otherwise the exception is unhandled and can crash the process.

Q: How do you detect memory leaks with EventEmitter?
A: Use emitter.eventNames() to list registered events and emitter.listenerCount('eventName') to count listeners. Node logs a warning automatically when a single event has more than 10 listeners.

Q: Implement a minimal EventEmitter with off support.
A: Use a Map<string, Set<Function>>. on adds to the set, off deletes from it (O(1) removal by reference), and emit iterates Array.from(set) to avoid mutation issues during the loop.

Examples

Pub-sub with multiple listeners

js
const EventEmitter = require('events'); const bus = new EventEmitter(); // Two independent listeners on the same event bus.on('login', (user) => console.log(`Send welcome email to ${user.email}`)); bus.on('login', (user) => console.log(`Track login for user ${user.id}`)); bus.emit('login', { id: 42, email: 'alice@example.com' }); // → Send welcome email to alice@example.com // → Track login for user 42

Both listeners fire in registration order. Neither knows the other exists. This is the decoupling that makes EventEmitter worth reaching for.

Express request logger using EventEmitter

js
const EventEmitter = require('events'); const express = require('express'); const app = express(); const logger = new EventEmitter(); logger.on('request', ({ method, url }) => { console.log(`${method} ${url} at ${new Date().toISOString()}`); }); app.use((req, res, next) => { logger.emit('request', { method: req.method, url: req.url }); next(); }); app.get('/users', (req, res) => res.send('Users list')); app.listen(3000); // GET /users → GET /users at 2024-01-15T10:30:00.000Z

The route handler has no idea logging exists. Swap out the logger listener any time without touching route code.

Extending EventEmitter for a file watcher

js
const EventEmitter = require('events'); const fs = require('fs'); class FileWatcher extends EventEmitter { watch(filePath) { fs.watchFile(filePath, { interval: 500 }, (curr, prev) => { if (curr.mtime > prev.mtime) { this.emit('change', { path: filePath, modified: curr.mtime }); } }); } } const watcher = new FileWatcher(); watcher.on('error', (err) => console.error('Watcher error:', err)); watcher.on('change', ({ path, modified }) => { console.log(`${path} changed at ${modified}`); }); watcher.watch('./config.json');

The class emits 'change' when the file updates. Callers decide what to do with that signal - hot reload, re-parse the config, notify a dashboard.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?