What this module covers: The EventEmitter class is the foundation of Node.js's architecture. HTTP servers, file streams, database connections, WebSockets — they all inherit from EventEmitter. You have already used it without knowing it: every time you called server.listen() or req.on('data', ...), you were using an EventEmitter. This module explains how EventEmitter works, how to build your own, how to avoid the memory leak that trips up every developer, and how the pattern underpins Node.js internals. Understanding this bridges Foundation to everything in the Practitioner and Architect phases.
What Is an Event Emitter?
An EventEmitter is an object that:
Lets you register named listeners — functions to call when a specific event occurs
Lets you emit events by name — which synchronously calls all registered listeners
It is the observer pattern, built into the Node.js runtime.
An EventEmitter is a radio station, not a phone call — it broadcasts on a named frequency without knowing or caring who's tuned in. A Promise is a phone call — one caller, one answer, then it's over.
javascript
Simple enough. The power comes from the fact that the emitter and the listener are decoupled — the code that fires the event does not know who is listening, and the listener does not know when or why the event was fired.
Core EventEmitter API
javascript
Building Your Own EventEmitter Subclass
The real pattern: extend EventEmitter in your own classes. This is how Node.js HTTP servers, streams, and virtually every I/O object are built.
javascript
Output:
text
The caller does not need to know anything about the pipeline's internals. It just listens for named events.
The error Event Is Special
If an EventEmitter emits an 'error' event and there is no listener registered for it, Node.js throws the error and crashes the process. This is not optional — it is a design decision to force you to handle errors explicitly.
javascript
Any class extending EventEmitter should either:
Register a default 'error' listener in the constructor, or
Document clearly that callers must register one
The Memory Leak Warning
This is the single most common EventEmitter mistake. By default, Node.js warns if you register more than 10 listeners for the same event on the same emitter:
text
Why? If you accidentally register a new listener on every request without removing the old one, you have a memory leak — the listener count grows unboundedly and the listeners are never garbage collected.
javascript
Production story: a real-time settlement-webhook processor built its own pub/sub on top of a single shared EventEmitter, and registered a new listener per incoming request without ever removing it. At roughly 2K TPS, the listener count climbed into the tens of thousands within 36 hours, and the pod was eventually OOM-killed. This is the exact failure MaxListenersExceededWarning exists to catch — except someone had silenced it early on by raising defaultMaxListeners to make the warning go away, rather than fixing the leak it was pointing at. Raising the limit is sometimes the right call (see below), but only when the higher listener count is deliberate and bounded — not as a way to stop the warning from firing.
The fix: remove listeners when you are done with them.
javascript
If you genuinely need many listeners (e.g. many subscribers to a shared emitter), increase the limit:
javascript
But increasing the limit should be a deliberate decision with a comment explaining why — not a way to silence the warning.
Synchronous vs Asynchronous Listeners
EventEmitter is synchronous by default. emit() calls all listeners immediately, in the order they were registered, before returning. There is no queuing or async scheduling:
javascript
This means if a listener does heavy synchronous work, it blocks the event loop — same as any other synchronous code. Keep listeners fast, or offload heavy work:
javascript
Note: if a listener is async and throws, the rejection is unhandled by default. The emitter does not await its listeners. For async event handling in production, always add try/catch inside the listener or use a wrapper.
captureRejections: Node's Built-in Fix
Node has an actual built-in answer to that "unhandled by default" problem: captureRejections. Enable it per-emitter or globally, and a rejected promise from an async listener is routed to the emitter's 'error' event instead of becoming an unhandled rejection:
javascript
javascript
This doesn't replace try/catch inside listeners that need custom recovery logic, but as a safety net it turns a silent, hard-to-trace unhandled rejection into a normal, handleable 'error' event — worth turning on by default in most services.
Async-Friendly Helpers: events.once() and events.on()
Alongside the EventEmitter class, node:events exports two standalone helpers that make waiting for events feel like ordinary async/await code instead of manual listener registration.
events.once(emitter, name) — wait for a single event as a Promise:
javascript
This is the async/await equivalent of emitter.once('ready', callback) — useful anywhere you would otherwise hand-wrap a one-off event in new Promise((resolve) => emitter.once(...)).
events.on(emitter, name) — async-iterate over every occurrence of an event:
javascript
This turns a stream of events into an async iterable you can for await over — a natural fit for the "many events over time" pattern this module keeps coming back to, without registering and later removing a callback by hand.
How Node.js Uses EventEmitter Internally
You have been using EventEmitters since F-1 without realising it. Every major I/O object in Node.js extends EventEmitter:
HTTP Server:
javascript
http.createServer() returns an http.Server which extends EventEmitter. The request callback shorthand (createServer(handler)) is simply .on('request', handler).
HTTP Request (IncomingMessage):
javascript
File Streams:
javascript
Process itself:
javascript
process is itself an EventEmitter.
EventEmitter vs Callbacks vs Promises
You now have three async patterns. When to use each:
Pattern
Best For
Callbacks
Simple one-time async operations. Legacy APIs.
Promises / async-await
One-time operations with a clear success/failure. Most application code.
EventEmitter
Multiple events over time. Streams of data. Pub/sub within a process. Multiple listeners for the same event.
The key distinction: Promises resolve once. EventEmitters fire many times. A file read finishes once → Promise. A server receives many requests → EventEmitter. A data pipeline processes thousands of records → EventEmitter.
Practical Pattern: Internal Event Bus
A lightweight event bus for decoupling modules inside a single Node.js process:
javascript
javascript
javascript
javascript
This pattern — emit from routes, listen in separate modules — keeps business logic out of HTTP handlers and makes the system easy to extend. Adding a new action when a user is created means adding a new listener file, not modifying the route.
once() for one-time listeners. off() to remove them. listenerCount() to check how many are attached.
Extend EventEmitter for your own classes that emit events over time (pipelines, connection managers, worker pools).
Always register an 'error' listener. An unhandled 'error' event crashes the process.
Memory leaks happen when you register listeners without removing them. Use off() or once() appropriately.
EventEmitter is synchronous. Listeners run inline when emit() is called. Async listeners need try/catch inside them, or captureRejections: true as a safety net.
events.once() and events.on() are async-friendly helpers — await a single event as a Promise, or for await iterate over every occurrence.
Node.js internals use EventEmitter everywhere — HTTP server, IncomingMessage, streams, process itself.
Use EventEmitters for streams of events over time. Use Promises for one-time results. Use callbacks only when working with legacy APIs.
This completes Phase 1 — Foundation. You now have the mental models, tools, and patterns to understand and build basic Node.js applications. Phase 2 takes all of this and builds it into production-quality application architecture: authentication, TypeScript, testing, security, and deployment.
Phase 1 recap: Runtime model → Module system → File I/O → Async programming → npm → Express APIs → Databases → Project structure → Event-driven core. Every concept in Phase 2 and Phase 3 builds directly on one or more of these.
Knowledge Check
What happens if an EventEmitter instance emits an 'error' event and there is no listener registered for it?
Which of the following best describes how EventEmitter executes its listeners when emit() is called?
What is the primary cause of the MaxListenersExceededWarning in Node.js applications?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
import{EventEmitter}from'node:events';const emitter =newEventEmitter();// Register a listener for the 'greet' eventemitter.on('greet',(name)=>{console.log(`Hello, ${name}!`);});// Emit the event — calls all registered 'greet' listenersemitter.emit('greet','Jatin');// → Hello, Jatin!emitter.emit('greet','World');// → Hello, World!
import{EventEmitter}from'node:events';const emitter =newEventEmitter();// on() — listen for every occurrence of an eventemitter.on('data',(chunk)=>{console.log('Received:', chunk);});// once() — listen only once, then automatically remove the listeneremitter.once('connected',()=>{console.log('Connected to database');});// emit() — fire an event with optional argumentsemitter.emit('data',Buffer.from('hello'));emitter.emit('connected');emitter.emit('connected');// second emit — the 'once' listener is gone// off() / removeListener() — remove a specific listenerfunctiononError(err){console.error('Error:', err.message);}emitter.on('error', onError);emitter.off('error', onError);// remove it later// removeAllListeners() — remove all listeners for an event (or all events)emitter.removeAllListeners('data');emitter.removeAllListeners();// clear everything// listenerCount() — how many listeners are registered for an eventconsole.log(emitter.listenerCount('data'));// 0// eventNames() — list all events that have listenersconsole.log(emitter.eventNames());
const emitter =newEventEmitter();// ❌ This will crash the processemitter.emit('error',newError('Something went wrong'));// → Uncaught Error: Something went wrong// ✅ Always register an error listeneremitter.on('error',(err)=>{console.error('Emitter error:', err.message);});emitter.emit('error',newError('Something went wrong'));// → Emitter error: Something went wrong
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 'data' listeners added to [EventEmitter].
// ❌ BAD — registers a new listener on every callapp.get('/stream',(req, res)=>{ pipeline.on('record',(record)=>{ res.write(JSON.stringify(record));});});// After 11 requests: MaxListenersExceededWarning// After 1000 requests: 1000 listeners, memory leak
// ✅ CORRECT — remove the listener when the request endsapp.get('/stream',(req, res)=>{functiononRecord(record){ res.write(JSON.stringify(record)+'\n');} pipeline.on('record', onRecord);// Remove when client disconnects req.on('close',()=>{ pipeline.off('record', onRecord);});});
emitter.setMaxListeners(50);// or globally:EventEmitter.defaultMaxListeners=50;
emitter.on('ping',()=>console.log('listener 1'));emitter.on('ping',()=>console.log('listener 2'));console.log('before emit');emitter.emit('ping');console.log('after emit');// Output:// before emit// listener 1// listener 2// after emit
// ❌ Blocks the event loopemitter.on('request',(data)=>{const result =heavyCpuWork(data);// blocks everythingsaveToDb(result);});// ✅ Don't block — hand off async workemitter.on('request',async(data)=>{const result =awaitheavyCpuWork(data);// if asyncawaitsaveToDb(result);});
// Per-emitterconst emitter =newEventEmitter({captureRejections:true});emitter.on('data',async(chunk)=>{thrownewError('boom');// rejects — captured instead of unhandled});emitter.on('error',(err)=>{console.error('Caught via captureRejections:', err.message);});emitter.emit('data','x');
// Globally, for every EventEmitter created afterward in the processimport{EventEmitter}from'node:events';EventEmitter.captureRejections=true;
import{EventEmitter, once }from'node:events';const emitter =newEventEmitter();setTimeout(()=> emitter.emit('ready','ok'),100);const[result]=awaitonce(emitter,'ready');// resolves when 'ready' firesconsole.log(result);// 'ok'
import{ on }from'node:events';asyncfunctionprocessRecords(pipeline){forawait(const[record]ofon(pipeline,'record')){console.log('Processing:', record);// break out of the loop whenever you want to stop iterating}}
importhttpfrom'node:http';const server = http.createServer();// These are EventEmitter.on() callsserver.on('request',(req, res)=>{ res.end('Hello');});server.on('error',(err)=>{console.error('Server error:', err);});server.on('close',()=>{console.log('Server closed');});server.listen(3000);
server.on('request',(req, res)=>{let body =''; req.on('data',(chunk)=>{// stream of data chunks body += chunk.toString();}); req.on('end',()=>{// all data receivedconsole.log('Body:', body); res.end('Received');}); req.on('error',(err)=>{console.error('Request error:', err);});});
importfsfrom'node:fs';const readable = fs.createReadStream('./large-file.csv');readable.on('data',(chunk)=>{// process chunk});readable.on('end',()=>{console.log('File fully read');});readable.on('error',(err)=>{console.error('Stream error:', err);});
process.on('exit',(code)=>{console.log('Exiting with code:', code);});process.on('SIGTERM',()=>{console.log('Received SIGTERM — shutting down'); process.exit(0);});
// src/events/bus.jsimport{EventEmitter}from'node:events';// Singleton event bus — shared across the applicationexportconst bus =newEventEmitter();bus.setMaxListeners(50);// Event name constants — prevents typosexportconstEvents={USER_CREATED:'user.created',POST_PUBLISHED:'post.published',ORDER_PLACED:'order.placed',EMAIL_REQUESTED:'email.requested',};
// src/routes/users.jsimport{ bus,Events}from'../events/bus.js';router.post('/',async(req, res, next)=>{try{const user =await prisma.user.create({data: req.body}); res.status(201).json(user);// Emit after responding — don't block the response bus.emit(Events.USER_CREATED, user);}catch(err){next(err);}});
// src/listeners/email.jsimport{ bus,Events}from'../events/bus.js';bus.on(Events.USER_CREATED,async(user)=>{try{awaitsendWelcomeEmail(user.email, user.name);}catch(err){console.error('Failed to send welcome email:', err.message);// Don't crash the process — log and move on}});
// src/index.js — register listeners at startupimport'./listeners/email.js';import'./listeners/analytics.js';