Back to Blog
Node.jsEventsEventEmitterBackend

Node.js Event Emitter Guide

Master Node.js EventEmitter for building event-driven applications.

B
Bootspring Team
Engineering
June 17, 2018
7 min read

The EventEmitter class is the foundation of Node.js event-driven architecture, enabling communication between objects through events.

Basic Usage#

1import { EventEmitter } from 'events'; 2 3// Create an emitter 4const emitter = new EventEmitter(); 5 6// Listen for event 7emitter.on('greet', (name) => { 8 console.log(`Hello, ${name}!`); 9}); 10 11// Emit event 12emitter.emit('greet', 'Alice'); 13// Output: Hello, Alice! 14 15// Multiple listeners 16emitter.on('greet', (name) => { 17 console.log(`Welcome, ${name}!`); 18}); 19 20emitter.emit('greet', 'Bob'); 21// Output: Hello, Bob! 22// Output: Welcome, Bob!

Event Methods#

1const emitter = new EventEmitter(); 2 3// on() - add listener 4emitter.on('data', (data) => console.log(data)); 5 6// once() - listen only once 7emitter.once('connect', () => { 8 console.log('Connected!'); 9}); 10 11emitter.emit('connect'); // Logs: Connected! 12emitter.emit('connect'); // Nothing happens 13 14// addListener() - alias for on() 15emitter.addListener('data', handler); 16 17// prependListener() - add to beginning 18emitter.prependListener('data', firstHandler); 19 20// prependOnceListener() 21emitter.prependOnceListener('init', initHandler);

Removing Listeners#

1const emitter = new EventEmitter(); 2 3function handler(data) { 4 console.log(data); 5} 6 7emitter.on('data', handler); 8 9// Remove specific listener 10emitter.off('data', handler); 11// or 12emitter.removeListener('data', handler); 13 14// Remove all listeners for event 15emitter.removeAllListeners('data'); 16 17// Remove all listeners 18emitter.removeAllListeners(); 19 20// Get listener count 21emitter.listenerCount('data'); 22 23// Get listeners 24emitter.listeners('data'); // Array of functions

Error Handling#

1const emitter = new EventEmitter(); 2 3// Always add error listener 4emitter.on('error', (err) => { 5 console.error('Error occurred:', err.message); 6}); 7 8// Emitting errors 9emitter.emit('error', new Error('Something went wrong')); 10 11// Without error listener, error events throw 12const unsafeEmitter = new EventEmitter(); 13unsafeEmitter.emit('error', new Error('Crash!')); // Throws! 14 15// Error handling pattern 16class SafeEmitter extends EventEmitter { 17 emit(event, ...args) { 18 if (event === 'error' && !this.listenerCount('error')) { 19 console.error('Unhandled error:', args[0]); 20 return false; 21 } 22 return super.emit(event, ...args); 23 } 24}

Custom Event Emitter Class#

1import { EventEmitter } from 'events'; 2 3class DataStream extends EventEmitter { 4 constructor() { 5 super(); 6 this.data = []; 7 } 8 9 push(item) { 10 this.data.push(item); 11 this.emit('data', item); 12 13 if (this.data.length >= 10) { 14 this.emit('threshold', this.data.length); 15 } 16 } 17 18 clear() { 19 this.data = []; 20 this.emit('clear'); 21 } 22 23 async fetch(url) { 24 try { 25 this.emit('fetching', url); 26 const response = await fetch(url); 27 const data = await response.json(); 28 this.emit('fetched', data); 29 return data; 30 } catch (error) { 31 this.emit('error', error); 32 throw error; 33 } 34 } 35} 36 37// Usage 38const stream = new DataStream(); 39 40stream.on('data', item => console.log('New item:', item)); 41stream.on('threshold', count => console.log(`Reached ${count} items`)); 42stream.on('error', err => console.error('Error:', err)); 43 44stream.push({ id: 1, value: 'test' });

Async Events#

1import { EventEmitter, once } from 'events'; 2 3const emitter = new EventEmitter(); 4 5// Wait for single event with promise 6async function waitForData() { 7 const [data] = await once(emitter, 'data'); 8 console.log('Received:', data); 9 return data; 10} 11 12// Usage 13setTimeout(() => { 14 emitter.emit('data', { message: 'Hello' }); 15}, 1000); 16 17await waitForData(); 18 19// Async iterator for events (Node.js 13+) 20import { on } from 'events'; 21 22async function processEvents() { 23 const asyncIterator = on(emitter, 'message'); 24 25 for await (const [message] of asyncIterator) { 26 console.log('Message:', message); 27 28 if (message === 'stop') { 29 break; 30 } 31 } 32} 33 34// With timeout 35import { setTimeout as delay } from 'timers/promises'; 36 37async function waitWithTimeout() { 38 const ac = new AbortController(); 39 40 setTimeout(() => ac.abort(), 5000); 41 42 try { 43 const [data] = await once(emitter, 'data', { 44 signal: ac.signal 45 }); 46 return data; 47 } catch (err) { 48 if (err.name === 'AbortError') { 49 throw new Error('Timeout waiting for data'); 50 } 51 throw err; 52 } 53}

Event Patterns#

1// Pub/Sub pattern 2class EventBus extends EventEmitter { 3 subscribe(event, callback) { 4 this.on(event, callback); 5 return () => this.off(event, callback); // Unsubscribe function 6 } 7 8 publish(event, data) { 9 this.emit(event, data); 10 } 11} 12 13const bus = new EventBus(); 14 15const unsubscribe = bus.subscribe('user:login', (user) => { 16 console.log('User logged in:', user.name); 17}); 18 19bus.publish('user:login', { name: 'Alice' }); 20unsubscribe(); // Stop listening 21 22// Wildcard events (custom implementation) 23class WildcardEmitter extends EventEmitter { 24 emit(event, ...args) { 25 super.emit(event, ...args); 26 27 // Emit to wildcard listeners 28 const parts = event.split(':'); 29 while (parts.length > 0) { 30 parts.pop(); 31 const wildcard = parts.join(':') + ':*'; 32 super.emit(wildcard, event, ...args); 33 } 34 super.emit('*', event, ...args); 35 } 36} 37 38const wild = new WildcardEmitter(); 39wild.on('user:*', (event, data) => { 40 console.log(`User event: ${event}`, data); 41}); 42wild.emit('user:login', { id: 1 }); 43wild.emit('user:logout', { id: 1 });

Configuration#

1import { EventEmitter } from 'events'; 2 3const emitter = new EventEmitter(); 4 5// Set max listeners (default is 10) 6emitter.setMaxListeners(20); 7 8// Get max listeners 9emitter.getMaxListeners(); 10 11// Global default 12EventEmitter.defaultMaxListeners = 15; 13 14// Get event names 15emitter.on('data', () => {}); 16emitter.on('error', () => {}); 17emitter.eventNames(); // ['data', 'error'] 18 19// Warning for too many listeners 20// Node.js warns when > maxListeners 21emitter.on('newListener', (event, listener) => { 22 console.log(`Adding listener for ${event}`); 23}); 24 25emitter.on('removeListener', (event, listener) => { 26 console.log(`Removed listener for ${event}`); 27});

Real-World Examples#

1// HTTP server events 2import http from 'http'; 3 4const server = http.createServer(); 5 6server.on('request', (req, res) => { 7 res.end('Hello World'); 8}); 9 10server.on('listening', () => { 11 console.log('Server is listening'); 12}); 13 14server.on('error', (err) => { 15 console.error('Server error:', err); 16}); 17 18server.on('close', () => { 19 console.log('Server closed'); 20}); 21 22server.listen(3000); 23 24// Stream events 25import fs from 'fs'; 26 27const readStream = fs.createReadStream('file.txt'); 28 29readStream.on('data', (chunk) => { 30 console.log('Received chunk:', chunk.length); 31}); 32 33readStream.on('end', () => { 34 console.log('Finished reading'); 35}); 36 37readStream.on('error', (err) => { 38 console.error('Read error:', err); 39}); 40 41// Database connection events 42class Database extends EventEmitter { 43 constructor(config) { 44 super(); 45 this.config = config; 46 this.connected = false; 47 } 48 49 async connect() { 50 try { 51 this.emit('connecting'); 52 // Simulate connection 53 await new Promise(r => setTimeout(r, 100)); 54 this.connected = true; 55 this.emit('connected'); 56 } catch (error) { 57 this.emit('error', error); 58 } 59 } 60 61 async query(sql) { 62 if (!this.connected) { 63 throw new Error('Not connected'); 64 } 65 this.emit('query', sql); 66 // Execute query... 67 this.emit('result', { rows: [] }); 68 } 69 70 disconnect() { 71 this.connected = false; 72 this.emit('disconnected'); 73 } 74} 75 76const db = new Database({ host: 'localhost' }); 77db.on('connecting', () => console.log('Connecting...')); 78db.on('connected', () => console.log('Connected!')); 79db.on('query', sql => console.log('Executing:', sql));

Memory Management#

1import { EventEmitter } from 'events'; 2 3// Avoid memory leaks 4class Component extends EventEmitter { 5 constructor(eventBus) { 6 super(); 7 this.eventBus = eventBus; 8 this.handlers = []; 9 } 10 11 init() { 12 // Store reference to handler 13 const handler = (data) => this.handleData(data); 14 this.handlers.push({ event: 'data', handler }); 15 this.eventBus.on('data', handler); 16 } 17 18 handleData(data) { 19 console.log('Data:', data); 20 } 21 22 destroy() { 23 // Clean up all handlers 24 for (const { event, handler } of this.handlers) { 25 this.eventBus.off(event, handler); 26 } 27 this.handlers = []; 28 this.removeAllListeners(); 29 } 30} 31 32// Using AbortController for cleanup 33class ManagedEmitter extends EventEmitter { 34 listen(event, handler, signal) { 35 this.on(event, handler); 36 37 if (signal) { 38 signal.addEventListener('abort', () => { 39 this.off(event, handler); 40 }); 41 } 42 43 return () => this.off(event, handler); 44 } 45} 46 47// Usage with AbortController 48const controller = new AbortController(); 49const emitter = new ManagedEmitter(); 50 51emitter.listen('data', handleData, controller.signal); 52 53// Later: cleanup all listeners 54controller.abort();

TypeScript Support#

1import { EventEmitter } from 'events'; 2 3// Typed events 4interface MyEvents { 5 data: (payload: { id: number; value: string }) => void; 6 error: (error: Error) => void; 7 complete: () => void; 8} 9 10class TypedEmitter extends EventEmitter { 11 on<K extends keyof MyEvents>( 12 event: K, 13 listener: MyEvents[K] 14 ): this { 15 return super.on(event, listener); 16 } 17 18 emit<K extends keyof MyEvents>( 19 event: K, 20 ...args: Parameters<MyEvents[K]> 21 ): boolean { 22 return super.emit(event, ...args); 23 } 24} 25 26const emitter = new TypedEmitter(); 27 28emitter.on('data', (payload) => { 29 console.log(payload.id); // Type-safe 30}); 31 32emitter.emit('data', { id: 1, value: 'test' }); // Type-checked

Best Practices#

Event Design: ✓ Use descriptive event names ✓ Namespace events (user:login) ✓ Document event payloads ✓ Always handle 'error' events Memory Management: ✓ Remove listeners when done ✓ Use once() for one-time events ✓ Set appropriate maxListeners ✓ Clean up in destroy/cleanup methods Error Handling: ✓ Always add error listener ✓ Use try/catch in async handlers ✓ Emit errors, don't throw ✓ Log unhandled errors Performance: ✓ Avoid too many listeners ✓ Use removeAllListeners sparingly ✓ Consider event pooling for high volume ✓ Profile listener execution time Avoid: ✗ Anonymous functions (can't remove) ✗ Memory leaks from listeners ✗ Throwing in event handlers ✗ Ignoring error events

Conclusion#

The EventEmitter is central to Node.js async architecture. Use it to build decoupled, event-driven systems. Always handle error events, clean up listeners to prevent memory leaks, and consider using once() for single-use events. The async utilities (once, on) enable promise-based event handling. For TypeScript, create typed emitter classes for better type safety.

Share this article

Help spread the word about Bootspring