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