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 happensCustom 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// SecondMax 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 subscriptionTyped 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.