Back to Blog
Node.jsEventsEventEmitterArchitecture

Node.js Events Module Guide

Master the Node.js events module for building event-driven applications.

B
Bootspring Team
Engineering
December 6, 2018
6 min read

The events module provides the EventEmitter class for building event-driven applications. Here's how to use it effectively.

Basic EventEmitter#

1import { EventEmitter } from 'node:events'; 2 3// Create emitter 4const emitter = new EventEmitter(); 5 6// Listen for event 7emitter.on('message', (data) => { 8 console.log('Received:', data); 9}); 10 11// Emit event 12emitter.emit('message', 'Hello, World!'); 13// Received: Hello, World! 14 15// Multiple arguments 16emitter.on('user:login', (user, timestamp) => { 17 console.log(`${user.name} logged in at ${timestamp}`); 18}); 19 20emitter.emit('user:login', { name: 'John' }, new Date());

Once Listeners#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5// Listen only once 6emitter.once('connect', () => { 7 console.log('Connected!'); 8}); 9 10emitter.emit('connect'); // Connected! 11emitter.emit('connect'); // Nothing happens

Custom Event Emitter Class#

1import { EventEmitter } from 'node:events'; 2 3class Server extends EventEmitter { 4 constructor() { 5 super(); 6 this.connections = []; 7 } 8 9 start(port) { 10 // Simulate starting server 11 setTimeout(() => { 12 this.emit('listening', port); 13 }, 100); 14 } 15 16 addConnection(client) { 17 this.connections.push(client); 18 this.emit('connection', client); 19 } 20 21 close() { 22 this.emit('close'); 23 this.connections = []; 24 } 25} 26 27// Usage 28const server = new Server(); 29 30server.on('listening', (port) => { 31 console.log(`Server listening on port ${port}`); 32}); 33 34server.on('connection', (client) => { 35 console.log(`New connection: ${client.id}`); 36}); 37 38server.on('close', () => { 39 console.log('Server closed'); 40}); 41 42server.start(3000); 43server.addConnection({ id: 'client-1' });

Error Handling#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5// Always handle error events 6emitter.on('error', (error) => { 7 console.error('Error occurred:', error.message); 8}); 9 10// Emit error 11emitter.emit('error', new Error('Something went wrong')); 12 13// Unhandled errors crash the process 14// If no error listener, emit('error') throws 15 16// Check for error listeners 17if (emitter.listenerCount('error') === 0) { 18 emitter.on('error', (err) => console.error(err)); 19}

Async Events#

1import { EventEmitter } from 'node:events'; 2 3class AsyncEmitter extends EventEmitter { 4 async emitAsync(event, ...args) { 5 const listeners = this.listeners(event); 6 for (const listener of listeners) { 7 await listener(...args); 8 } 9 } 10} 11 12const emitter = new AsyncEmitter(); 13 14emitter.on('process', async (data) => { 15 await new Promise(resolve => setTimeout(resolve, 100)); 16 console.log('Processed:', data); 17}); 18 19// Wait for async handlers 20await emitter.emitAsync('process', { id: 1 }); 21console.log('Done');

Event Names and Listeners#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5emitter.on('event1', () => {}); 6emitter.on('event1', () => {}); 7emitter.on('event2', () => {}); 8 9// Get event names 10console.log(emitter.eventNames()); // ['event1', 'event2'] 11 12// Get listener count 13console.log(emitter.listenerCount('event1')); // 2 14 15// Get listeners 16const listeners = emitter.listeners('event1'); 17console.log(listeners.length); // 2 18 19// Get raw listeners (includes once wrappers) 20const rawListeners = emitter.rawListeners('event1');

Removing Listeners#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5function handler(data) { 6 console.log(data); 7} 8 9// Add listener 10emitter.on('data', handler); 11 12// Remove specific listener 13emitter.off('data', handler); 14// or 15emitter.removeListener('data', handler); 16 17// Remove all listeners for event 18emitter.removeAllListeners('data'); 19 20// Remove all listeners for all events 21emitter.removeAllListeners();

Prepending Listeners#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5emitter.on('message', () => console.log('Second')); 6 7// Add to beginning of listener array 8emitter.prependListener('message', () => console.log('First')); 9 10// Once at beginning 11emitter.prependOnceListener('message', () => console.log('Once First')); 12 13emitter.emit('message'); 14// Once First 15// First 16// Second

Max Listeners#

1import { EventEmitter } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5// Default is 10 listeners per event 6// Get warning if exceeded 7 8// Set max listeners 9emitter.setMaxListeners(20); 10 11// Get current max 12console.log(emitter.getMaxListeners()); // 20 13 14// Set for all emitters 15EventEmitter.defaultMaxListeners = 15; 16 17// Unlimited (use carefully) 18emitter.setMaxListeners(0);

Event-Based Queue#

1import { EventEmitter } from 'node:events'; 2 3class TaskQueue extends EventEmitter { 4 constructor(concurrency = 1) { 5 super(); 6 this.concurrency = concurrency; 7 this.running = 0; 8 this.queue = []; 9 } 10 11 push(task) { 12 this.queue.push(task); 13 this.emit('task:added', task); 14 this.process(); 15 } 16 17 async process() { 18 while (this.running < this.concurrency && this.queue.length > 0) { 19 const task = this.queue.shift(); 20 this.running++; 21 this.emit('task:start', task); 22 23 try { 24 const result = await task(); 25 this.emit('task:complete', result); 26 } catch (error) { 27 this.emit('task:error', error); 28 } finally { 29 this.running--; 30 this.process(); 31 } 32 } 33 34 if (this.running === 0 && this.queue.length === 0) { 35 this.emit('drain'); 36 } 37 } 38} 39 40// Usage 41const queue = new TaskQueue(2); 42 43queue.on('task:complete', (result) => console.log('Done:', result)); 44queue.on('drain', () => console.log('All tasks complete')); 45 46queue.push(async () => { 47 await delay(100); 48 return 'Task 1'; 49});

Pub/Sub Pattern#

1import { EventEmitter } from 'node:events'; 2 3class PubSub extends EventEmitter { 4 subscribe(channel, callback) { 5 this.on(channel, callback); 6 return () => this.off(channel, callback); 7 } 8 9 publish(channel, data) { 10 this.emit(channel, data); 11 } 12 13 unsubscribe(channel, callback) { 14 if (callback) { 15 this.off(channel, callback); 16 } else { 17 this.removeAllListeners(channel); 18 } 19 } 20} 21 22// Usage 23const pubsub = new PubSub(); 24 25const unsubscribe = pubsub.subscribe('news', (article) => { 26 console.log('New article:', article.title); 27}); 28 29pubsub.publish('news', { title: 'Breaking News!' }); 30 31unsubscribe(); // Remove subscription

Typed Events (TypeScript)#

1import { EventEmitter } from 'node:events'; 2 3interface UserEvents { 4 login: [user: User]; 5 logout: [userId: string]; 6 error: [error: Error]; 7} 8 9class TypedEmitter<T extends Record<string, any[]>> { 10 private emitter = new EventEmitter(); 11 12 on<K extends keyof T>(event: K, listener: (...args: T[K]) => void) { 13 this.emitter.on(event as string, listener); 14 return this; 15 } 16 17 emit<K extends keyof T>(event: K, ...args: T[K]) { 18 return this.emitter.emit(event as string, ...args); 19 } 20 21 off<K extends keyof T>(event: K, listener: (...args: T[K]) => void) { 22 this.emitter.off(event as string, listener); 23 return this; 24 } 25} 26 27// Usage 28const userEmitter = new TypedEmitter<UserEvents>(); 29 30userEmitter.on('login', (user) => { 31 console.log(user.name); // TypeScript knows user type 32}); 33 34userEmitter.emit('login', { id: '1', name: 'John' });

Promise-Based Events#

1import { EventEmitter, once } from 'node:events'; 2 3const emitter = new EventEmitter(); 4 5// Wait for event with promise 6async function waitForConnection() { 7 const [socket] = await once(emitter, 'connect'); 8 console.log('Connected:', socket); 9} 10 11// With timeout 12async function waitWithTimeout(event, timeout) { 13 const ac = new AbortController(); 14 const timeoutId = setTimeout(() => ac.abort(), timeout); 15 16 try { 17 const result = await once(emitter, event, { signal: ac.signal }); 18 clearTimeout(timeoutId); 19 return result; 20 } catch (error) { 21 if (error.name === 'AbortError') { 22 throw new Error(`Timeout waiting for ${event}`); 23 } 24 throw error; 25 } 26}

Best Practices#

Patterns: ✓ Always handle 'error' events ✓ Use descriptive event names ✓ Namespace events (user:login) ✓ Document event signatures Memory: ✓ Remove listeners when done ✓ Use once() for single events ✓ Set appropriate maxListeners ✓ Clean up in destroy methods Async: ✓ Handle async errors ✓ Use once() for promises ✓ Consider event ordering ✓ Emit after state changes Avoid: ✗ Circular event emissions ✗ Heavy work in handlers ✗ Ignoring error events ✗ Memory leaks from listeners

Conclusion#

The events module provides a solid foundation for event-driven architecture in Node.js. Extend EventEmitter for custom classes, always handle error events, and manage listeners properly to avoid memory leaks. Use typed events in TypeScript for better developer experience and use the once function for promise-based event handling.

Share this article

Help spread the word about Bootspring