The events Module — EventEmitter Pattern in Node.js

▶ Try It Yourself

The events module and the EventEmitter class are the backbone of Node.js. Almost every core module — http, fs, stream, net, child_process — inherits from EventEmitter. It is the Observer pattern built directly into the runtime. Understanding EventEmitter means you understand how Node.js servers receive requests, how streams signal data arrival, how databases signal query completion, and how you can build your own loosely-coupled, event-driven systems. In the MEAN Stack, it is the foundation for Socket.io real-time events, Express middleware, and Node.js stream-based file handling.

EventEmitter API

Method Purpose
emitter.on(event, listener) Register a persistent listener — called every time event fires
emitter.once(event, listener) Register a one-time listener — auto-removed after first call
emitter.off(event, listener) Remove a specific listener — alias for removeListener
emitter.emit(event, ...args) Fire event synchronously — all listeners called immediately
emitter.removeAllListeners(event?) Remove all listeners for an event (or all events)
emitter.listeners(event) Returns array of listener functions for an event
emitter.listenerCount(event) Returns number of listeners for an event
emitter.setMaxListeners(n) Change the warning threshold (default: 10)
emitter.eventNames() Returns array of registered event names

Important Behaviours

Behaviour Detail
Synchronous emission emit() calls all listeners synchronously in registration order
Error events are special If 'error' is emitted with no listener, Node.js throws and crashes
Memory leak warning Adding more than 10 listeners to one event triggers a warning
newListener event Emitted before a listener is added — useful for introspection
removeListener event Emitted after a listener is removed
Note: emit() is synchronous — it calls all registered listeners immediately, in the order they were added, before returning. This is different from DOM events where dispatch is asynchronous in some contexts. If you need async listeners, you must manually make them async: emitter.on('event', async () => { await doSomething(); }) — but be aware that the emitter does not wait for the Promise to resolve.
Tip: Always store a reference to listener functions when you need to remove them later. Arrow functions create a new function object each time — emitter.off('data', () => handler()) will fail to remove the listener because it is a different function object than the one that was registered. Store the listener in a variable: const listener = () => handler(); emitter.on('data', listener); emitter.off('data', listener);
Warning: Always add an 'error' event listener to every EventEmitter. Node.js treats an unhandled 'error' event as a fatal error — it throws the error object, which will crash your process if not caught by a top-level handler. This is especially important for stream and socket objects: stream.on('error', err => console.error(err)).

Basic Example

const EventEmitter = require('events');

// ── Creating a custom EventEmitter ────────────────────────────────────────
class OrderProcessor extends EventEmitter {
    async process(order) {
        this.emit('processing', order);

        try {
            const result = await this.#validateOrder(order);
            this.emit('validated', result);

            const payment = await this.#chargePayment(result);
            this.emit('paid', payment);

            await this.#fulfillOrder(payment);
            this.emit('fulfilled', { orderId: order.id, status: 'complete' });

        } catch (err) {
            this.emit('error', err);   // MUST be handled — otherwise Node.js crashes
        }
    }

    async #validateOrder(order)   { /* ... */ return order; }
    async #chargePayment(order)   { /* ... */ return order; }
    async #fulfillOrder(order)    { /* ... */ }
}

const processor = new OrderProcessor();

// Register listeners
processor.on('processing', order  => console.log('Processing order:', order.id));
processor.on('validated',  order  => console.log('Order validated:', order.id));
processor.on('paid',       payment=> console.log('Payment received:', payment.id));
processor.on('fulfilled',  result => console.log('Order complete:', result.orderId));
processor.on('error',      err    => console.error('Order failed:', err.message));

// Process an order
processor.process({ id: 'ORD-001', total: 99.99 });

// ── on vs once ────────────────────────────────────────────────────────────
const emitter = new EventEmitter();

emitter.on('data', chunk => console.log('on — fires every time:', chunk));
emitter.once('connect', () => console.log('once — fires only the first time'));

emitter.emit('data', 'chunk 1');   // on fires
emitter.emit('data', 'chunk 2');   // on fires
emitter.emit('connect');            // once fires
emitter.emit('connect');            // once does NOT fire again

// ── Removing listeners ────────────────────────────────────────────────────
const emitter2 = new EventEmitter();

const handleData = (data) => console.log('Received:', data);

emitter2.on('data', handleData);
emitter2.emit('data', 'hello');    // fires

emitter2.off('data', handleData);  // remove it
emitter2.emit('data', 'hello');    // does NOT fire — listener removed

// ── Listener count and memory leak prevention ─────────────────────────────
const emitter3 = new EventEmitter();
emitter3.setMaxListeners(20);       // suppress warning for expected high counts

for (let i = 0; i < 15; i++) {
    emitter3.on('message', msg => console.log(i, msg));
}

console.log(emitter3.listenerCount('message'));  // 15
console.log(emitter3.eventNames());              // ['message']

// ── Error event handling ──────────────────────────────────────────────────
const safeEmitter = new EventEmitter();

// ALWAYS register an error listener
safeEmitter.on('error', (err) => {
    console.error('Handled error:', err.message);
    // process continues
});

safeEmitter.emit('error', new Error('Something went wrong'));
// Without the listener above — Node.js would throw and crash

Real-World Example: Application Event Bus

// event-bus.js — application-wide event bus (Singleton)
const EventEmitter = require('events');

class EventBus extends EventEmitter {
    constructor() {
        super();
        this.setMaxListeners(50);

        // Always handle errors
        this.on('error', err => {
            console.error('[EventBus] Unhandled error:', err);
        });
    }

    // Type-safe emit with logging
    publish(event, payload) {
        console.log(`[EventBus] Publishing: ${event}`, payload);
        this.emit(event, payload);
    }

    // Subscribe with automatic cleanup on 'destroy'
    subscribe(event, listener, { once = false } = {}) {
        once ? this.once(event, listener) : this.on(event, listener);
        return () => this.off(event, listener);   // return unsubscribe fn
    }
}

// Export singleton
module.exports = new EventBus();

// ── Usage across modules ───────────────────────────────────────────────────
// auth.controller.js
const bus = require('./event-bus');

async function login(req, res) {
    const user = await AuthService.login(req.body);
    bus.publish('user.login', { userId: user.id, ip: req.ip, ts: Date.now() });
    res.json({ token: generateToken(user) });
}

// audit.service.js — reacts to login events
const bus = require('./event-bus');

const unsubscribe = bus.subscribe('user.login', async ({ userId, ip, ts }) => {
    await AuditLog.create({ userId, event: 'login', ip, timestamp: ts });
    console.log(`[Audit] User ${userId} logged in from ${ip}`);
});

// notification.service.js — also reacts to login
bus.subscribe('user.login', ({ userId }) => {
    console.log(`[Notifications] Checking alerts for user ${userId}`);
});

// Clean up on process shutdown
process.on('SIGTERM', () => {
    unsubscribe();
    bus.removeAllListeners();
});

Common Mistakes

Mistake 1 — Using anonymous arrow functions that cannot be removed

❌ Wrong — cannot call off() with an anonymous function:

emitter.on('data', (d) => process(d));
// Later:
emitter.off('data', (d) => process(d));   // DOES NOT WORK — different function object

✅ Correct — store a reference to remove it later:

const handler = (d) => process(d);
emitter.on('data', handler);
// Later:
emitter.off('data', handler);   // works — same reference

Mistake 2 — Not handling the error event

❌ Wrong — crashes the Node.js process:

const stream = fs.createReadStream('./nonexistent-file.txt');
stream.on('data', chunk => process(chunk));
// stream emits 'error' — no listener — Node.js THROWS and process crashes

✅ Correct — always add an error listener to every EventEmitter:

const stream = fs.createReadStream('./file.txt');
stream.on('data',  chunk => process(chunk));
stream.on('error', err   => console.error('Stream error:', err.message));
stream.on('end',   ()    => console.log('Done'));

Mistake 3 — Async listeners where errors are swallowed silently

❌ Wrong — async listener error is swallowed, emitter never knows it failed:

emitter.on('order', async (order) => {
    await processOrder(order);   // if this throws — unhandled rejection!
});

✅ Correct — catch errors inside async listeners and emit or log them:

emitter.on('order', async (order) => {
    try {
        await processOrder(order);
    } catch (err) {
        emitter.emit('error', err);   // propagate to error listener
    }
});

Quick Reference

Task Code
Create custom emitter class MyEmitter extends EventEmitter {}
Listen persistently emitter.on('event', handler)
Listen once emitter.once('event', handler)
Remove listener emitter.off('event', handler)
Fire event emitter.emit('event', payload)
Count listeners emitter.listenerCount('event')
List event names emitter.eventNames()
Prevent leak warning emitter.setMaxListeners(20)
Remove all listeners emitter.removeAllListeners('event')

🧠 Test Yourself

What happens if a Node.js EventEmitter emits an 'error' event and no 'error' listener is registered?





▶ Try It Yourself