Chapter 28: Node.js Events
1. What is the events module and why is it so important?
The events module is one of the most fundamental pieces of Node.js.
Almost everything asynchronous in Node.js is built on top of the EventEmitter class that lives in this module.
EventEmitter is the class that lets objects:
- emit (trigger) named events
- listen (subscribe) to those events
- remove listeners when no longer needed
Why it’s everywhere:
- http.Server is an EventEmitter (emits ‘request’, ‘error’, ‘close’, …)
- Streams (Readable, Writable, Duplex) are EventEmitters
- net.Server, net.Socket, child_process.ChildProcess, process itself — all inherit from EventEmitter
- Most third-party libraries (WebSocket, Redis clients, database drivers, queues…) use EventEmitter or a similar pattern
In simple words:
EventEmitter = a pub-sub system built into Node.js core You say: “When this happens, please call my function”
2. Modern import style (2025–2026)
|
0 1 2 3 4 5 6 7 8 9 |
import { EventEmitter } from 'node:events'; // or the whole module import events from 'node:events'; |
Always prefer the node: prefix — clearer and safer.
3. Basic usage – the classic example
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// events-basic.js import { EventEmitter } from 'node:events'; const myEmitter = new EventEmitter(); // 1. Subscribe (listen) to an event myEmitter.on('user:login', (user) => { console.log(`${user.name} (${user.id}) just logged in at ${new Date().toLocaleTimeString()}`); }); // You can have multiple listeners for the same event myEmitter.on('user:login', (user) => { console.log(`→ Sending welcome email to ${user.email}`); }); // 2. Emit (trigger) the event myEmitter.emit('user:login', { id: 123, name: 'Aman', email: 'aman@example.com' }); // Output: // Aman (123) just logged in at 2:45:12 PM // → Sending welcome email to aman@example.com |
4. Most important methods – with realistic examples
4.1 .on(eventName, listener) – subscribe (persistent)
|
0 1 2 3 4 5 6 7 8 9 |
// Most common method — listener stays forever myEmitter.on('file:uploaded', (fileInfo) => { console.log(`New upload: ${fileInfo.name} (${fileInfo.size} bytes)`); }); |
4.2 .once(eventName, listener) – subscribe only once
|
0 1 2 3 4 5 6 7 8 9 |
// Listener is removed automatically after first call myEmitter.once('server:ready', () => { console.log('Server is ready → sending startup notification'); }); |
Very useful for one-time initialization, first connection, etc.
4.3 .emit(eventName, …args) – trigger the event
|
0 1 2 3 4 5 6 7 8 9 10 11 |
// Can pass any number of arguments myEmitter.emit('order:created', { orderId: 'ORD-98765', amount: 1499.99, items: ['Laptop', 'Mouse'] }, 'credit_card'); |
4.4 .off(eventName, listener) / .removeListener() – unsubscribe
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
const expensiveListener = (data) => { // do heavy work... }; myEmitter.on('data:received', expensiveListener); // Later — remove it myEmitter.off('data:received', expensiveListener); |
Tip: Always keep a reference to the listener function if you plan to remove it later.
4.5 .removeAllListeners([eventName]) – remove everything
|
0 1 2 3 4 5 6 7 8 |
// Careful — usually only used in tests or cleanup myEmitter.removeAllListeners(); // remove all events myEmitter.removeAllListeners('error'); // remove only error listeners |
4.6 .listeners(eventName) & .listenerCount(eventName) – inspection
|
0 1 2 3 4 5 6 7 |
console.log(myEmitter.listenerCount('user:login')); // → 2 console.log(myEmitter.listeners('user:login')); // → array of functions |
Very useful for debugging or monitoring.
4.7 Special events: ‘error’, ‘newListener’, ‘removeListener’
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Very important: every EventEmitter should handle 'error' myEmitter.on('error', (err) => { console.error('Critical error in emitter:', err.message); // Usually: log + graceful shutdown or notify admin }); // Useful for debugging listener leaks myEmitter.on('newListener', (eventName, listener) => { console.log(`New listener added for event: ${eventName}`); }); |
Rule in production code:
Always add at least one ‘error’ listener Unhandled ‘error’ events will crash the process
5. Real-world patterns you’ll see in 2025–2026
Pattern 1 – Application-wide event bus (very common)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// events.js import { EventEmitter } from 'node:events'; const appEvents = new EventEmitter(); // Prevent memory leak warning in development appEvents.setMaxListeners(20); // default is 10 export default appEvents; |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// user-service.js import appEvents from './events.js'; export async function loginUser(credentials) { // ... login logic ... appEvents.emit('user:login', { id: user.id, name: user.name, email: user.email, ip: req.ip }); } |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// email-service.js import appEvents from './events.js'; appEvents.on('user:login', (user) => { // sendWelcomeEmail(user.email); console.log(`Email would be sent to ${user.email}`); }); |
Pattern 2 – Class that extends EventEmitter (classic style)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class FileWatcher extends EventEmitter { constructor(filePath) { super(); this.filePath = filePath; this.startWatching(); } startWatching() { // fake watcher setInterval(() => { this.emit('change', { file: this.filePath, time: new Date() }); }, 5000); } } const watcher = new FileWatcher('config.json'); watcher.on('change', (info) => { console.log(`File changed: ${info.file}`); }); |
Pattern 3 – Safe error handling + cleanup
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const emitter = new EventEmitter(); // Always add error handler emitter.on('error', (err) => { console.error('Emitter error:', err); // optional: emitter.removeAllListeners(); }); // When cleaning up (tests, shutdown) function cleanup() { emitter.removeAllListeners(); } |
6. Common mistakes & how to avoid them
| Mistake | Consequence | Fix / Best Practice |
|---|---|---|
| Forgetting ‘error’ listener | Process crashes on any emit(‘error’) | Always add at least one ‘error’ handler |
| Adding too many listeners | Memory leak warning (default max 10) | Use .setMaxListeners(n) or .once() |
| Not removing listeners when done | Memory leaks in long-running apps | Keep references & use .off() or .removeAllListeners() |
| Emitting ‘error’ without object | Hard to debug | Always emit(‘error’, new Error(‘message’)) |
| Using arrow functions as listeners | Cannot remove them later | Use named functions if you plan to .off() |
Summary – Quick decision guide 2025–2026
| You want to… | Best approach | Typical real-world use case |
|---|---|---|
| Create custom events | new EventEmitter() | File watcher, task queue, app-wide notifications |
| Make a class event-driven | class MyClass extends EventEmitter {} | Streams, servers, database clients, custom tools |
| Listen only once | .once() | Initialization, first connection, one-time actions |
| Safely clean up listeners | .off() / .removeAllListeners() | Tests, graceful shutdown, component unmount |
| Handle errors properly | Always have ‘error’ listener | Prevent silent failures & crashes |
Would you like to go much deeper into any part?
- Building a real application-wide event bus with namespaces
- Using EventEmitter with TypeScript (strongly typed events)
- Memory leak debugging patterns with EventEmitter
- Comparison: EventEmitter vs modern alternatives (mitt, tiny-emitter, eventemitter3)
- Complete example – file watcher + logger + notifier using events
Just tell me which direction feels most useful right now — I’ll continue with production-ready, copy-paste examples. 😊
