Back to Blog
Node.jsEventEmitterEventsPatterns

Node.js EventEmitter Patterns

Master Node.js EventEmitter. From basic events to typed emitters to real-world patterns.

B
Bootspring Team
Engineering
April 26, 2021
7 min read

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 sequentially

Memory 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.

Share this article

Help spread the word about Bootspring