EventEmitter is the foundation of Node.js event-driven architecture. Here's how to use it effectively.
Basic Usage#
1const { EventEmitter } = require('events');
2
3const emitter = new EventEmitter();
4
5// Listen for event
6emitter.on('greet', (name) => {
7 console.log(`Hello, ${name}!`);
8});
9
10// Emit event
11emitter.emit('greet', 'World'); // Hello, World!
12
13// Listen once
14emitter.once('init', () => {
15 console.log('Initialized');
16});
17
18emitter.emit('init'); // Initialized
19emitter.emit('init'); // Nothing happens
20
21// Remove listener
22const handler = () => console.log('Handler');
23emitter.on('event', handler);
24emitter.off('event', handler);
25// Or: emitter.removeListener('event', handler);Custom EventEmitter Class#
1const { EventEmitter } = require('events');
2
3class TaskRunner extends EventEmitter {
4 constructor() {
5 super();
6 this.tasks = [];
7 }
8
9 addTask(task) {
10 this.tasks.push(task);
11 this.emit('taskAdded', task);
12 }
13
14 async run() {
15 this.emit('start', { totalTasks: this.tasks.length });
16
17 for (let i = 0; i < this.tasks.length; i++) {
18 const task = this.tasks[i];
19
20 try {
21 this.emit('taskStart', { task, index: i });
22 const result = await task();
23 this.emit('taskComplete', { task, index: i, result });
24 } catch (error) {
25 this.emit('taskError', { task, index: i, error });
26 }
27 }
28
29 this.emit('complete', { totalTasks: this.tasks.length });
30 }
31}
32
33// Usage
34const runner = new TaskRunner();
35
36runner.on('start', ({ totalTasks }) => {
37 console.log(`Starting ${totalTasks} tasks`);
38});
39
40runner.on('taskComplete', ({ index, result }) => {
41 console.log(`Task ${index + 1} completed:`, result);
42});
43
44runner.on('complete', () => {
45 console.log('All tasks completed');
46});
47
48runner.addTask(async () => 'Task 1 result');
49runner.addTask(async () => 'Task 2 result');
50runner.run();TypeScript Typed Events#
1import { EventEmitter } from 'events';
2
3// Define event types
4interface ServerEvents {
5 connection: [socket: Socket];
6 message: [data: string, sender: string];
7 error: [error: Error];
8 close: [];
9}
10
11// Typed EventEmitter
12class TypedEventEmitter<T extends Record<string, any[]>> {
13 private emitter = new EventEmitter();
14
15 on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this {
16 this.emitter.on(event as string, listener);
17 return this;
18 }
19
20 off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this {
21 this.emitter.off(event as string, listener);
22 return this;
23 }
24
25 emit<K extends keyof T>(event: K, ...args: T[K]): boolean {
26 return this.emitter.emit(event as string, ...args);
27 }
28
29 once<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this {
30 this.emitter.once(event as string, listener);
31 return this;
32 }
33}
34
35// Usage
36class Server extends TypedEventEmitter<ServerEvents> {
37 start() {
38 // Types are enforced
39 this.emit('connection', new Socket());
40 this.emit('message', 'Hello', 'user1');
41 // this.emit('message', 123); // Error: number not assignable to string
42 }
43}
44
45const server = new Server();
46
47server.on('message', (data, sender) => {
48 // data: string, sender: string (inferred)
49 console.log(`${sender}: ${data}`);
50});Error Handling#
1const { EventEmitter } = require('events');
2
3const emitter = new EventEmitter();
4
5// Always handle error events
6emitter.on('error', (err) => {
7 console.error('Error occurred:', err.message);
8});
9
10// Without error handler, unhandled error crashes the process
11emitter.emit('error', new Error('Something went wrong'));
12
13// Error handling in async operations
14class AsyncProcessor extends EventEmitter {
15 async process(data) {
16 try {
17 const result = await this.doWork(data);
18 this.emit('success', result);
19 } catch (error) {
20 this.emit('error', error);
21 }
22 }
23}
24
25// Capture unhandled rejections in event handlers
26const { captureRejectionSymbol } = require('events');
27
28class SafeEmitter extends EventEmitter {
29 [captureRejectionSymbol](error, event) {
30 console.error(`Rejection in ${event} handler:`, error);
31 this.emit('error', error);
32 }
33}
34
35// Or globally
36EventEmitter.captureRejections = true;Async Iteration#
1const { EventEmitter, on } = require('events');
2
3const emitter = new EventEmitter();
4
5// Async iterator for events
6async function processEvents() {
7 const iterator = on(emitter, 'data');
8
9 for await (const [data] of iterator) {
10 console.log('Received:', data);
11
12 if (data === 'stop') {
13 break;
14 }
15 }
16
17 console.log('Stopped processing');
18}
19
20processEvents();
21
22// Emit events
23emitter.emit('data', 'first');
24emitter.emit('data', 'second');
25emitter.emit('data', 'stop');
26
27// With AbortController
28const { once } = require('events');
29
30async function waitForEvent() {
31 const ac = new AbortController();
32
33 setTimeout(() => ac.abort(), 5000); // Timeout
34
35 try {
36 const [value] = await once(emitter, 'data', { signal: ac.signal });
37 console.log('Received:', value);
38 } catch (err) {
39 if (err.code === 'ABORT_ERR') {
40 console.log('Timed out waiting for event');
41 }
42 }
43}Event Namespacing#
1class NamespacedEmitter extends EventEmitter {
2 emitNamespaced(namespace, event, ...args) {
3 this.emit(`${namespace}:${event}`, ...args);
4 this.emit(`${namespace}:*`, event, ...args);
5 }
6
7 onNamespaced(namespace, event, handler) {
8 if (event === '*') {
9 this.on(`${namespace}:*`, handler);
10 } else {
11 this.on(`${namespace}:${event}`, handler);
12 }
13 return this;
14 }
15}
16
17const emitter = new NamespacedEmitter();
18
19// Listen to specific event
20emitter.onNamespaced('user', 'login', (data) => {
21 console.log('User logged in:', data);
22});
23
24// Listen to all events in namespace
25emitter.onNamespaced('user', '*', (event, data) => {
26 console.log(`User event: ${event}`, data);
27});
28
29emitter.emitNamespaced('user', 'login', { userId: 1 });
30emitter.emitNamespaced('user', 'logout', { userId: 1 });Pub/Sub Pattern#
1class PubSub extends EventEmitter {
2 constructor() {
3 super();
4 this.channels = new Map();
5 }
6
7 subscribe(channel, handler) {
8 if (!this.channels.has(channel)) {
9 this.channels.set(channel, new Set());
10 }
11
12 this.channels.get(channel).add(handler);
13 this.on(channel, handler);
14
15 // Return unsubscribe function
16 return () => {
17 this.channels.get(channel).delete(handler);
18 this.off(channel, handler);
19 };
20 }
21
22 publish(channel, message) {
23 this.emit(channel, message);
24 }
25
26 getSubscriberCount(channel) {
27 return this.channels.get(channel)?.size || 0;
28 }
29}
30
31const pubsub = new PubSub();
32
33const unsubscribe = pubsub.subscribe('notifications', (msg) => {
34 console.log('Notification:', msg);
35});
36
37pubsub.publish('notifications', { type: 'info', text: 'Hello' });
38
39unsubscribe();Event Queuing#
1class QueuedEmitter extends EventEmitter {
2 constructor() {
3 super();
4 this.queue = [];
5 this.processing = false;
6 }
7
8 enqueue(event, ...args) {
9 this.queue.push({ event, args });
10 this.processQueue();
11 }
12
13 async processQueue() {
14 if (this.processing) return;
15 this.processing = true;
16
17 while (this.queue.length > 0) {
18 const { event, args } = this.queue.shift();
19
20 // Wait for all handlers
21 const listeners = this.listeners(event);
22 await Promise.all(
23 listeners.map(listener => listener(...args))
24 );
25 }
26
27 this.processing = false;
28 }
29}
30
31const emitter = new QueuedEmitter();
32
33emitter.on('task', async (task) => {
34 await new Promise(resolve => setTimeout(resolve, 100));
35 console.log('Processed:', task);
36});
37
38emitter.enqueue('task', 'Task 1');
39emitter.enqueue('task', 'Task 2');
40emitter.enqueue('task', 'Task 3');
41// Tasks processed sequentiallyMemory Management#
1const { EventEmitter } = require('events');
2
3// Set max listeners (default is 10)
4const emitter = new EventEmitter();
5emitter.setMaxListeners(20);
6
7// Or globally
8EventEmitter.defaultMaxListeners = 20;
9
10// Check listener count
11console.log(emitter.listenerCount('event'));
12
13// Get all listeners
14const listeners = emitter.listeners('event');
15
16// Remove all listeners
17emitter.removeAllListeners('event');
18// Or all events: emitter.removeAllListeners();
19
20// Prepend listener (add to front)
21emitter.prependListener('event', () => {
22 console.log('This runs first');
23});
24
25// Track listener changes
26emitter.on('newListener', (event, listener) => {
27 console.log(`Added listener for ${event}`);
28});
29
30emitter.on('removeListener', (event, listener) => {
31 console.log(`Removed listener for ${event}`);
32});Real-World Example: File Watcher#
1const { EventEmitter } = require('events');
2const fs = require('fs');
3const path = require('path');
4
5class FileWatcher extends EventEmitter {
6 constructor(directory) {
7 super();
8 this.directory = directory;
9 this.watcher = null;
10 }
11
12 start() {
13 this.watcher = fs.watch(this.directory, (eventType, filename) => {
14 if (!filename) return;
15
16 const filepath = path.join(this.directory, filename);
17
18 fs.stat(filepath, (err, stats) => {
19 if (err) {
20 if (err.code === 'ENOENT') {
21 this.emit('delete', { filename, filepath });
22 } else {
23 this.emit('error', err);
24 }
25 return;
26 }
27
28 if (eventType === 'rename') {
29 this.emit('create', { filename, filepath, stats });
30 } else {
31 this.emit('change', { filename, filepath, stats });
32 }
33 });
34 });
35
36 this.emit('start', { directory: this.directory });
37 }
38
39 stop() {
40 if (this.watcher) {
41 this.watcher.close();
42 this.emit('stop');
43 }
44 }
45}
46
47// Usage
48const watcher = new FileWatcher('./watched');
49
50watcher.on('start', ({ directory }) => {
51 console.log(`Watching ${directory}`);
52});
53
54watcher.on('change', ({ filename }) => {
55 console.log(`Changed: ${filename}`);
56});
57
58watcher.on('create', ({ filename }) => {
59 console.log(`Created: ${filename}`);
60});
61
62watcher.start();Best Practices#
Design:
✓ Always handle 'error' events
✓ Use typed events in TypeScript
✓ Document event signatures
✓ Clean up listeners when done
Performance:
✓ Remove listeners when not needed
✓ Set appropriate max listeners
✓ Use once() for one-time events
✓ Avoid synchronous operations in handlers
Patterns:
✓ Emit events for state changes
✓ Use namespaced events for organization
✓ Return unsubscribe functions
✓ Consider async iteration for streams
Conclusion#
EventEmitter enables loose coupling and async communication in Node.js. Use typed events for safety, handle errors properly, and clean up listeners to prevent memory leaks. It's ideal for building modular, event-driven architectures.