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
  2. EventEmitter Class
  3. Working with Events
  4. Advanced Event Patterns
  5. Error Handling
  6. Real-World Examples
  7. Performance & Best Practices
  8. Quick Reference
  9. Interview Q&A + MCQ

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

MethodDescription
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&A
1What 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()?easy
Answer: on() runs every time the event emits; once() auto-removes after first execution.
3Why is the error event special?medium
Answer: 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?medium
Answer: 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?

Anode:events
Bnode:emit
Cnode:eventloop
Dnode:queue
Explanation: EventEmitter is provided by node:events.
2

Which method registers one-time listeners?

Aon()
Bonce()
Csingle()
Donly()
Explanation: once() auto-removes listener after first call.
3

If error is emitted without a listener, what happens?

AIgnored silently
BNode may throw and crash
CRetries automatically
DCreates a default logger
Explanation: Always attach an error listener for safety.
4

Which method places a listener at the start?

AprependListener()
BfirstListener()
CstartOn()
DpriorityOn()
Explanation: prependListener() inserts before existing listeners.
5

What does listenerCount(event) return?

ANumber of emitted events
BNumber of listeners for an event
CQueue size
DNumber of errors
Explanation: It gives listener count for that event.
6

Default EventEmitter listener execution is:

AAlways async
BSynchronous
CWorker thread based
DRandom order
Explanation: Listeners run synchronously in registration order.
7

Recommended for one-time init event?

Aon()
Bonce()
Cemit()
Dlisteners()
Explanation: once() prevents duplicate handling.
8

Which API helps avoid blocking in heavy emit loops?

AsetImmediate()
BMath.random()
CsetInterval(0)
DBuffer.alloc()
Explanation: setImmediate() lets event loop breathe.
9

What is a good naming style for events?

AthingHappenedMaybe
Buser:registered
Cevent1
Dx
Explanation: Namespaced, descriptive names are easier to maintain.
10

Which method emits an event?

Adispatch()
Btrigger()
Cemit()
DfireEvent()
Explanation: Node EventEmitter uses emit().