Back to Blog
Node.jsEventsPatternsArchitecture

Node.js EventEmitter Patterns

Master the Node.js EventEmitter. From basics to custom emitters to advanced patterns.

B
Bootspring Team
Engineering
October 16, 2020
7 min read

The EventEmitter is central to Node.js architecture. Here's how to use it effectively.

EventEmitter Basics#

1const EventEmitter = require('events'); 2 3// Create an emitter 4const emitter = new EventEmitter(); 5 6// Listen for events 7emitter.on('message', (data) => { 8 console.log('Received:', data); 9}); 10 11// Emit events 12emitter.emit('message', 'Hello, World!'); 13 14// One-time listener 15emitter.once('connect', () => { 16 console.log('Connected (runs once)'); 17}); 18 19// Remove listener 20const handler = (data) => console.log(data); 21emitter.on('data', handler); 22emitter.off('data', handler); // or removeListener 23 24// Remove all listeners 25emitter.removeAllListeners('data'); 26emitter.removeAllListeners(); // All events

Creating Custom Emitters#

1const EventEmitter = require('events'); 2 3// Class-based emitter 4class Database extends EventEmitter { 5 constructor(config) { 6 super(); 7 this.config = config; 8 this.connected = false; 9 } 10 11 async connect() { 12 this.emit('connecting'); 13 14 try { 15 await this.doConnect(); 16 this.connected = true; 17 this.emit('connected'); 18 } catch (error) { 19 this.emit('error', error); 20 } 21 } 22 23 async query(sql) { 24 this.emit('query', sql); 25 26 try { 27 const result = await this.execute(sql); 28 this.emit('result', result); 29 return result; 30 } catch (error) { 31 this.emit('error', error); 32 throw error; 33 } 34 } 35 36 async doConnect() { 37 // Connection logic 38 } 39 40 async execute(sql) { 41 // Query execution 42 } 43} 44 45// Usage 46const db = new Database({ host: 'localhost' }); 47 48db.on('connecting', () => console.log('Connecting...')); 49db.on('connected', () => console.log('Connected!')); 50db.on('query', (sql) => console.log('Query:', sql)); 51db.on('error', (err) => console.error('Error:', err)); 52 53await db.connect(); 54await db.query('SELECT * FROM users');

Job Queue Pattern#

1const EventEmitter = require('events'); 2 3class JobQueue extends EventEmitter { 4 constructor(concurrency = 1) { 5 super(); 6 this.concurrency = concurrency; 7 this.queue = []; 8 this.running = 0; 9 } 10 11 add(job) { 12 this.queue.push(job); 13 this.emit('job:added', job); 14 this.process(); 15 } 16 17 async process() { 18 while (this.running < this.concurrency && this.queue.length > 0) { 19 const job = this.queue.shift(); 20 this.running++; 21 this.emit('job:started', job); 22 23 try { 24 const result = await job.execute(); 25 this.emit('job:completed', { job, result }); 26 } catch (error) { 27 this.emit('job:failed', { job, error }); 28 } finally { 29 this.running--; 30 this.process(); 31 } 32 } 33 34 if (this.running === 0 && this.queue.length === 0) { 35 this.emit('queue:empty'); 36 } 37 } 38} 39 40// Usage 41const queue = new JobQueue(3); 42 43queue.on('job:started', ({ id }) => console.log(`Job ${id} started`)); 44queue.on('job:completed', ({ job, result }) => console.log(`Job ${job.id} done`)); 45queue.on('job:failed', ({ job, error }) => console.error(`Job ${job.id} failed`)); 46queue.on('queue:empty', () => console.log('All jobs complete')); 47 48queue.add({ id: 1, execute: async () => 'Result 1' }); 49queue.add({ id: 2, execute: async () => 'Result 2' });

WebSocket-like Pattern#

1const EventEmitter = require('events'); 2 3class Connection extends EventEmitter { 4 constructor(id) { 5 super(); 6 this.id = id; 7 this.connected = false; 8 } 9 10 open() { 11 this.connected = true; 12 this.emit('open'); 13 } 14 15 send(message) { 16 if (!this.connected) { 17 this.emit('error', new Error('Not connected')); 18 return; 19 } 20 this.emit('send', message); 21 } 22 23 receive(message) { 24 this.emit('message', message); 25 } 26 27 close(code, reason) { 28 this.connected = false; 29 this.emit('close', { code, reason }); 30 } 31} 32 33class ConnectionManager extends EventEmitter { 34 constructor() { 35 super(); 36 this.connections = new Map(); 37 } 38 39 add(connection) { 40 this.connections.set(connection.id, connection); 41 42 connection.on('message', (msg) => { 43 this.emit('message', { connection, message: msg }); 44 }); 45 46 connection.on('close', () => { 47 this.connections.delete(connection.id); 48 this.emit('disconnect', connection); 49 }); 50 51 this.emit('connect', connection); 52 } 53 54 broadcast(message, exclude = []) { 55 for (const [id, conn] of this.connections) { 56 if (!exclude.includes(id)) { 57 conn.send(message); 58 } 59 } 60 } 61}

Event Bus Pattern#

1const EventEmitter = require('events'); 2 3class EventBus extends EventEmitter { 4 constructor() { 5 super(); 6 this.setMaxListeners(100); 7 } 8 9 // Namespaced events 10 publish(namespace, event, data) { 11 const fullEvent = `${namespace}:${event}`; 12 this.emit(fullEvent, data); 13 this.emit('*', { namespace, event, data }); 14 } 15 16 subscribe(namespace, event, handler) { 17 const fullEvent = `${namespace}:${event}`; 18 this.on(fullEvent, handler); 19 return () => this.off(fullEvent, handler); 20 } 21 22 // Wildcard subscription 23 subscribeAll(handler) { 24 this.on('*', handler); 25 return () => this.off('*', handler); 26 } 27} 28 29// Global event bus 30const eventBus = new EventBus(); 31 32// Module A 33const unsubscribe = eventBus.subscribe('users', 'created', (user) => { 34 console.log('User created:', user); 35}); 36 37// Module B 38eventBus.publish('users', 'created', { id: 1, name: 'Alice' }); 39 40// Cleanup 41unsubscribe();

State Machine with Events#

1const EventEmitter = require('events'); 2 3class StateMachine extends EventEmitter { 4 constructor(config) { 5 super(); 6 this.states = config.states; 7 this.state = config.initial; 8 } 9 10 transition(event) { 11 const currentStateConfig = this.states[this.state]; 12 const nextState = currentStateConfig?.on?.[event]; 13 14 if (!nextState) { 15 this.emit('transition:invalid', { from: this.state, event }); 16 return false; 17 } 18 19 const prevState = this.state; 20 this.state = nextState; 21 22 this.emit('transition', { from: prevState, to: nextState, event }); 23 this.emit(`enter:${nextState}`, { from: prevState }); 24 this.emit(`exit:${prevState}`, { to: nextState }); 25 26 return true; 27 } 28 29 getState() { 30 return this.state; 31 } 32 33 can(event) { 34 return !!this.states[this.state]?.on?.[event]; 35 } 36} 37 38// Usage 39const orderMachine = new StateMachine({ 40 initial: 'pending', 41 states: { 42 pending: { on: { PAY: 'paid', CANCEL: 'cancelled' } }, 43 paid: { on: { SHIP: 'shipped', REFUND: 'refunded' } }, 44 shipped: { on: { DELIVER: 'delivered' } }, 45 delivered: {}, 46 cancelled: {}, 47 refunded: {}, 48 }, 49}); 50 51orderMachine.on('transition', ({ from, to, event }) => { 52 console.log(`Order: ${from} -> ${to} (${event})`); 53}); 54 55orderMachine.on('enter:shipped', () => { 56 console.log('Send shipping notification'); 57}); 58 59orderMachine.transition('PAY'); 60orderMachine.transition('SHIP');

Error Handling#

1const EventEmitter = require('events'); 2 3class SafeEmitter extends EventEmitter { 4 // Emit with error handling 5 safeEmit(event, ...args) { 6 try { 7 this.emit(event, ...args); 8 } catch (error) { 9 this.emit('error', error); 10 } 11 } 12 13 // Async emit with error collection 14 async emitAsync(event, ...args) { 15 const listeners = this.listeners(event); 16 const errors = []; 17 18 for (const listener of listeners) { 19 try { 20 await listener(...args); 21 } catch (error) { 22 errors.push(error); 23 } 24 } 25 26 if (errors.length > 0) { 27 this.emit('errors', { event, errors }); 28 } 29 } 30} 31 32// Always handle error events 33const emitter = new EventEmitter(); 34 35emitter.on('error', (error) => { 36 console.error('EventEmitter error:', error); 37}); 38 39// Or use captureRejections 40const asyncEmitter = new EventEmitter({ captureRejections: true }); 41 42asyncEmitter.on('data', async () => { 43 throw new Error('Async error'); 44}); 45 46// This gets emitted as 'error' event 47asyncEmitter.emit('data');

Memory Management#

1const EventEmitter = require('events'); 2 3// Increase max listeners if needed 4const emitter = new EventEmitter(); 5emitter.setMaxListeners(20); // Default is 10 6 7// Check for leaks 8console.log(emitter.listenerCount('event')); 9 10// Properly cleanup 11class Component extends EventEmitter { 12 constructor(eventBus) { 13 super(); 14 this.eventBus = eventBus; 15 this.handlers = []; 16 } 17 18 initialize() { 19 const handler = this.onData.bind(this); 20 this.eventBus.on('data', handler); 21 this.handlers.push({ event: 'data', handler }); 22 } 23 24 onData(data) { 25 // Handle data 26 } 27 28 destroy() { 29 // Cleanup all handlers 30 for (const { event, handler } of this.handlers) { 31 this.eventBus.off(event, handler); 32 } 33 this.handlers = []; 34 this.removeAllListeners(); 35 } 36} 37 38// Auto-cleanup with AbortController 39const controller = new AbortController(); 40 41emitter.on('event', handler, { signal: controller.signal }); 42 43// Later, cleanup 44controller.abort();

Typed Events (TypeScript)#

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

Best Practices#

Design: ✓ Use semantic event names ✓ Document event payloads ✓ Keep events focused ✓ Consider event versioning Memory: ✓ Remove listeners when done ✓ Watch maxListeners warnings ✓ Use once() for one-time events ✓ Clean up in destroy methods Error Handling: ✓ Always add error listeners ✓ Handle async errors properly ✓ Use captureRejections ✓ Don't throw in listeners Performance: ✓ Don't over-emit ✓ Batch related events ✓ Consider throttling ✓ Profile with many listeners

Conclusion#

The EventEmitter pattern enables loose coupling and flexible architectures. Use it for async communication, state changes, and plugin systems. Always handle errors, manage memory properly, and document your event contracts.

Share this article

Help spread the word about Bootspring