Complete Node.js Events Tutorial
EventEmitter, advanced patterns, error handling, and production-ready event-driven design
Event-Driven
Real-Time Ready
Interview Focused
Table of Contents
1. Introduction to Events
What are Events?
Events are actions or occurrences in your app that your code can react to. Node.js uses this heavily for scalable I/O and real-time workflows.
// Simple analogy: Like waiting for a doorbell
door.on('ring', () => {
console.log('Someone is at the door!');
});
// Later, someone rings the doorbell
door.ring(); // Triggers the 'ring' event
Why Use Events?
- Decoupled architecture: components communicate without tight coupling
- Real-time responses: handle actions immediately
- Reusable modules: one emitter can serve many listeners
- Asynchronous friendliness: ideal for I/O-heavy Node.js systems
2. EventEmitter Class
Basic Setup
// Import the events module
const EventEmitter = require('node:events');
// Create an instance
const myEmitter = new EventEmitter();
// OR create your own class that extends EventEmitter
class MyEmitter extends EventEmitter {}
const myEmitter2 = new MyEmitter();
Core Methods Reference
| Method | Description |
|---|---|
on(eventName, listener) | Adds a listener for the specified event |
once(eventName, listener) | Adds a one-time listener and removes it after first call |
emit(eventName, ...args) | Triggers an event with optional arguments |
removeListener(eventName, listener) | Removes a specific listener |
off(eventName, listener) | Alias for removeListener |
removeAllListeners([eventName]) | Removes all listeners for one event or all events |
listeners(eventName) | Returns listener array |
listenerCount(eventName) | Returns total listeners for the event |
eventNames() | Returns all event names |
Your First Event
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
emitter.emit('greet', 'Alice');
emitter.on('greet', (name) => {
console.log(`Welcome to our app, ${name}!`);
});
emitter.emit('greet', 'Bob');
3. Working with Events
Different Ways to Create Listeners
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event1', () => console.log('Event 1 triggered'));
emitter.addListener('event2', () => console.log('Event 2 triggered'));
emitter.once('oneTime', () => console.log('This runs only once!'));
emitter.emit('oneTime');
emitter.emit('oneTime');
emitter.on('order', () => console.log('Second'));
emitter.prependListener('order', () => console.log('First'));
emitter.emit('order');
Passing Data with Events
const EventEmitter = require('node:events');
const orderSystem = new EventEmitter();
orderSystem.on('newOrder', (orderId) => {
console.log(`Processing order #${orderId}`);
});
orderSystem.on('orderDetails', (orderId, amount, customer) => {
console.log(`Order ${orderId}: ${customer} ordered $${amount}`);
});
orderSystem.on('completeOrder', (orderData) => {
console.log(`Order ${orderData.id} completed for ${orderData.customer}`);
});
orderSystem.emit('newOrder', 'ORD-123');
orderSystem.emit('orderDetails', 'ORD-456', 99.99, 'John Doe');
orderSystem.emit('completeOrder', { id: 'ORD-789', customer: 'Jane Smith', total: 149.99 });
Managing Listeners + Sync vs Async
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
const listener1 = () => console.log('Listener 1');
const listener2 = () => console.log('Listener 2');
emitter.on('test', listener1);
emitter.on('test', listener2);
console.log(emitter.listenerCount('test'));
emitter.removeListener('test', listener2);
emitter.emit('test');
emitter.on('async', () => {
setImmediate(() => console.log('Async operation completed'));
console.log('Listener started');
});
emitter.emit('async');
console.log('After emit');
4. Advanced Event Patterns
Creating Custom EventEmitter Classes (UserManager)
const EventEmitter = require('node:events');
class UserManager extends EventEmitter {
constructor() {
super();
this.users = new Map();
this.setMaxListeners(20); // Increase max listeners limit
}
addUser(userId, userData) {
if (this.users.has(userId)) {
this.emit('error', new Error(`User ${userId} already exists`));
return false;
}
this.users.set(userId, userData);
this.emit('userAdded', { userId, ...userData });
this.emit('userCount', this.users.size);
return true;
}
removeUser(userId) {
if (!this.users.has(userId)) {
this.emit('error', new Error(`User ${userId} not found`));
return false;
}
const userData = this.users.get(userId);
this.users.delete(userId);
this.emit('userRemoved', { userId, ...userData });
this.emit('userCount', this.users.size);
return true;
}
getUser(userId) {
return this.users.get(userId);
}
getAllUsers() {
return Array.from(this.users.values());
}
}
File Watcher System
const EventEmitter = require('node:events');
const fs = require('node:fs/promises');
const path = require('node:path');
class FileWatcher extends EventEmitter {
constructor(directory, interval = 5000) {
super();
this.directory = directory;
this.interval = interval;
this.fileStates = new Map();
this.watching = false;
}
async start() {
this.watching = true;
await this.scan();
this.timer = setInterval(() => this.scan(), this.interval);
this.emit('started', { directory: this.directory });
}
stop() {
this.watching = false;
if (this.timer) clearInterval(this.timer);
this.emit('stopped');
}
async scan() {
try {
const files = await fs.readdir(this.directory);
for (const file of files) {
const filePath = path.join(this.directory, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
const previousState = this.fileStates.get(file);
const currentState = {
size: stats.size,
modified: stats.mtime.getTime(),
exists: true
};
if (!previousState) {
this.emit('fileAdded', { name: file, path: filePath, stats });
} else if (previousState.modified !== currentState.modified) {
this.emit('fileChanged', {
name: file,
path: filePath,
oldSize: previousState.size,
newSize: currentState.size
});
}
this.fileStates.set(file, currentState);
}
}
for (const [file] of this.fileStates) {
try {
await fs.access(path.join(this.directory, file));
} catch {
this.emit('fileRemoved', { name: file });
this.fileStates.delete(file);
}
}
} catch (error) {
this.emit('error', error);
}
}
}
Event Chaining and Pipelines
class EventPipeline extends EventEmitter {
constructor() {
super();
this.pipes = [];
}
pipe(transform) {
this.pipes.push(transform);
return this;
}
async process(data) {
let result = data;
for (const pipe of this.pipes) {
try {
result = await pipe(result);
this.emit('stepComplete', { step: pipe.name, result });
} catch (error) {
this.emit('error', { error, data: result });
throw error;
}
}
this.emit('complete', { input: data, output: result });
return result;
}
}
5. Error Handling
Handling Error Events Safely
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// Always define an error handler
emitter.on('error', (error) => {
console.error('Caught error:', error.message);
});
emitter.emit('error', new Error('Something broke'));
Async Emit Pattern
class AsyncEventEmitter extends EventEmitter {
async emitAsync(event, ...args) {
const listeners = this.listeners(event);
const results = [];
for (const listener of listeners) {
try {
results.push(await listener(...args));
} catch (error) {
this.emit('error', error);
results.push(null);
}
}
return results;
}
}
6. Real-World Examples
Example 1: Webhook Dispatcher
const EventEmitter = require('node:events');
const https = require('node:https');
const http = require('node:http');
class WebhookManager extends EventEmitter {
constructor() {
super();
this.webhooks = new Map();
this.setMaxListeners(50);
}
registerWebhook(event, url, secret = null) {
if (!this.webhooks.has(event)) {
this.webhooks.set(event, []);
this.on(event, async (data) => {
await this.dispatchWebhook(event, data);
});
}
this.webhooks.get(event).push({ url, secret });
this.emit('webhookRegistered', { event, url });
}
async dispatchWebhook(event, payload) {
const webhooks = this.webhooks.get(event) || [];
const results = await Promise.allSettled(
webhooks.map(webhook => this.sendWebhook(webhook, payload))
);
this.emit('webhookDispatchComplete', { event, results });
}
async sendWebhook(webhook, payload) {
const url = new URL(webhook.url);
const data = JSON.stringify({
event: payload.event,
timestamp: new Date().toISOString(),
data: payload.data
});
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
'X-Webhook-Secret': webhook.secret || ''
}
};
return new Promise((resolve, reject) => {
const client = url.protocol === 'https:' ? https : http;
const req = client.request(options, (res) => {
let response = '';
res.on('data', chunk => response += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ success: true, statusCode: res.statusCode });
} else {
reject(new Error(`HTTP ${res.statusCode}: ${response}`));
}
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
triggerEvent(event, data) {
this.emit(event, { event, data, timestamp: new Date().toISOString() });
this.emit('eventTriggered', { event, data });
}
}
Example 2: Task Queue System
class TaskQueue extends EventEmitter {
constructor(concurrency = 3) {
super();
this.queue = [];
this.active = 0;
this.concurrency = concurrency;
this.completed = 0;
this.failed = 0;
}
addTask(task, priority = 0) {
const taskWithPriority = { task, priority, id: Date.now() + Math.random(), added: new Date() };
let inserted = false;
for (let i = 0; i < this.queue.length; i++) {
if (this.queue[i].priority < priority) {
this.queue.splice(i, 0, taskWithPriority);
inserted = true;
break;
}
}
if (!inserted) this.queue.push(taskWithPriority);
this.emit('taskAdded', { id: taskWithPriority.id, priority, queueLength: this.queue.length });
this.processQueue();
return taskWithPriority.id;
}
async processQueue() {
while (this.active < this.concurrency && this.queue.length > 0) {
const taskItem = this.queue.shift();
this.active++;
this.emit('taskStarted', { id: taskItem.id, active: this.active });
this.executeTask(taskItem).finally(() => {
this.active--;
this.emit('taskFinished', {
id: taskItem.id,
active: this.active,
completed: this.completed,
failed: this.failed
});
this.processQueue();
});
}
if (this.active === 0 && this.queue.length === 0) {
this.emit('queueDrained', { completed: this.completed, failed: this.failed });
}
}
async executeTask(taskItem) {
const startTime = Date.now();
try {
this.emit('taskProcessing', { id: taskItem.id });
const result = await taskItem.task();
const duration = Date.now() - startTime;
this.completed++;
this.emit('taskSuccess', { id: taskItem.id, result, duration });
return result;
} catch (error) {
this.failed++;
this.emit('taskError', { id: taskItem.id, error: error.message });
throw error;
}
}
}
Example 3: Real-time Chat System
class ChatRoom extends EventEmitter {
constructor(roomName) {
super();
this.roomName = roomName;
this.users = new Map();
this.messageHistory = [];
this.maxHistory = 100;
this.setMaxListeners(100);
}
join(userId, username) {
if (this.users.has(userId)) {
this.emit('error', new Error(`User ${userId} already in room`));
return false;
}
const user = { userId, username, joined: new Date() };
this.users.set(userId, user);
this.emit('userJoined', {
room: this.roomName,
user,
userCount: this.users.size,
users: Array.from(this.users.values())
});
if (this.messageHistory.length > 0) {
this.emit('history', {
user: userId,
messages: this.messageHistory.slice(-20)
});
}
return true;
}
leave(userId) {
if (!this.users.has(userId)) return false;
const user = this.users.get(userId);
this.users.delete(userId);
this.emit('userLeft', {
room: this.roomName,
user,
userCount: this.users.size
});
return true;
}
sendMessage(userId, message) {
const user = this.users.get(userId);
if (!user) {
this.emit('error', new Error('User not in room'));
return false;
}
const messageObj = {
id: Date.now(),
userId: user.userId,
username: user.username,
message: message.trim(),
timestamp: new Date(),
room: this.roomName
};
this.messageHistory.push(messageObj);
if (this.messageHistory.length > this.maxHistory) this.messageHistory.shift();
this.emit('message', messageObj);
this.emit('broadcast', messageObj);
return true;
}
}
7. Performance & Best Practices
Memory Leak Prevention
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.setMaxListeners(20);
class SafeEmitter extends EventEmitter {
constructor(maxListeners = 10) {
super();
this.setMaxListeners(maxListeners);
this.listenerWarnings = new Map();
}
on(event, listener) {
const currentCount = this.listenerCount(event);
const max = this.getMaxListeners();
if (currentCount + 1 > max) {
console.warn(`Warning: ${currentCount + 1} listeners for event '${event}'`);
if (!this.listenerWarnings.has(event)) {
this.listenerWarnings.set(event, Date.now());
}
}
return super.on(event, listener);
}
cleanup() {
const events = this.eventNames();
for (const event of events) this.removeAllListeners(event);
this.listenerWarnings.clear();
}
}
Best Practices Summary
// DO: Use descriptive event names
emitter.on('user:registered', handler);
emitter.on('order:processed', handler);
emitter.on('file:uploaded', handler);
// DON'T: Use vague names
emitter.on('thing', handler);
emitter.on('happened', handler);
// DO: Always handle errors
emitter.on('error', (error) => console.error(error));
// DO: Remove temporary listeners
emitter.off('data', handler);
// DO: Use once() when appropriate
emitter.once('ready', () => console.log('Ready'));
// DO: Use symbols for internal events to avoid collisions
const internalEvents = {
CACHE_HIT: Symbol('cache:hit'),
CACHE_MISS: Symbol('cache:miss')
};
// DON'T: Heavy synchronous loops can block event loop
for (let i = 0; i < 100000; i++) {
emitter.emit('data', i);
}
// DO: Defer emission for huge loops
for (let i = 0; i < 100000; i++) {
setImmediate(() => emitter.emit('data', i));
}
Quick Reference
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', handler);
emitter.once('event', handler);
emitter.prependListener('event', handler);
emitter.prependOnceListener('event', handler);
emitter.off('event', handler);
emitter.removeAllListeners('event');
emitter.emit('event', arg1, arg2);
emitter.listenerCount('event');
emitter.listeners('event');
emitter.eventNames();
emitter.getMaxListeners();
emitter.setMaxListeners(20);
class WildcardEmitter extends EventEmitter {
emit(event, ...args) {
super.emit(event, ...args);
if (event.includes(':')) {
const namespace = event.split(':')[0];
super.emit(`${namespace}:*`, event, ...args);
}
return super.emit('*', event, ...args);
}
}
10 Interview Questions + 10 MCQs
Interview Pattern 10 Q&A1What is the purpose of EventEmitter in Node.js?easy
Answer: It provides a publish-subscribe pattern where listeners react to emitted events.
2Difference between
on() and once()?easyAnswer:
on() runs every time the event emits; once() auto-removes after first execution.3Why is the
error event special?mediumAnswer: Emitting
error without a listener can crash the process.4How to avoid listener memory leaks?medium
Answer: Remove unused listeners, prefer
once() for one-shot events, and monitor listener counts.5How can you pass multiple values in an event?easy
Answer: Pass multiple arguments in
emit() or pass a structured object.6Are listeners synchronous or asynchronous by default?medium
Answer: Synchronous by default; use
setImmediate, nextTick, or async logic for deferred work.7What does
prependListener() do?mediumAnswer: It inserts a listener at the beginning so it runs before existing listeners.
8When should you extend EventEmitter?medium
Answer: When creating domain-specific components that emit internal lifecycle or state-change events.
9How would you run async listeners safely?hard
Answer: Use a custom
emitAsync() loop with try/catch around each listener and central error handling.10Give one production use case of event-driven architecture.hard
Answer: Webhook dispatchers, chat systems, task queues, and file watchers are common real-time use cases.
10 Node.js Events MCQs
1
Which module contains EventEmitter?
Explanation: EventEmitter is provided by
node:events.2
Which method registers one-time listeners?
Explanation:
once() auto-removes listener after first call.3If
If error is emitted without a listener, what happens?
Explanation: Always attach an
error listener for safety.4
Which method places a listener at the start?
Explanation:
prependListener() inserts before existing listeners.5What does
What does listenerCount(event) return?
Explanation: It gives listener count for that event.
6
Default EventEmitter listener execution is:
Explanation: Listeners run synchronously in registration order.
7
Recommended for one-time init event?
Explanation:
once() prevents duplicate handling.8
Which API helps avoid blocking in heavy emit loops?
Explanation:
setImmediate() lets event loop breathe.9
What is a good naming style for events?
Explanation: Namespaced, descriptive names are easier to maintain.
10
Which method emits an event?
Explanation: Node EventEmitter uses
emit().