Node.js is built around a single idea: instead of waiting for something to happen, you describe what should happen when it does. This is the event-driven model, and EventEmitter is the class that makes it work. When you call server.listen() or pipe a streamWhat is stream?A way to process data in small chunks as it arrives instead of loading everything into memory at once, keeping memory usage low for large files., EventEmitter is running the show behind the scenes.
Creating and using an EventEmitter
You can use EventEmitter directly or extend it in your own classes. Think of it like a radio station: .emit() broadcasts a signal and .on() tunes in.
import { EventEmitter } from 'events';
const emitter = new EventEmitter();
// Register a listener
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
// Trigger the event - can pass any number of arguments
emitter.emit('greet', 'Alice'); // Hello, Alice!
emitter.emit('greet', 'Bob'); // Hello, Bob!Core methods at a glance
| Method | What it does |
|---|---|
.on(event, fn) | Add a persistent listener |
.once(event, fn) | Add a listener that fires exactly once |
.emit(event, ...args) | Trigger all listeners for an event |
.off(event, fn) | Remove a specific listener |
.removeAllListeners(event) | Remove every listener for an event |
.listenerCount(event) | Count currently attached listeners |
.setMaxListeners(n) | Change the warning threshold (default: 10) |
// .once() is perfect for one-time setup
emitter.once('init', () => {
console.log('App initialized - this only runs once');
});
emitter.emit('init'); // Fires
emitter.emit('init'); // Silently ignored.once() removes itself automatically. You do not need to call .off() after it fires. This is useful for connection events, setup steps, or any signal that should only be acted on once.Building a class with events
The most powerful pattern is extending EventEmitter in your own class. Your class gains the full event APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. and can announce changes to any listener without needing a reference to them.
import { EventEmitter } from 'events';
class DataProcessor extends EventEmitter {
constructor() {
super();
this.queue = [];
}
async process(item) {
this.emit('start', item);
try {
const result = await this.transform(item);
this.emit('success', result);
return result;
} catch (error) {
this.emit('error', error);
throw error;
}
}
async transform(item) {
return { ...item, processed: true };
}
}
// Usage - the caller decides what to do with each event
const processor = new DataProcessor();
processor.on('start', (item) => console.log(`Processing: ${item.id}`));
processor.on('success', (result) => console.log(`Done: ${result.id}`));
processor.on('error', (err) => console.error('Failed:', err.message));
await processor.process({ id: 1, data: 'test' });This decoupling is the real power. DataProcessor does not know or care what happens after it emits, it just broadcasts the news.
Built-in EventEmitters you use every day
HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. server
import { createServer } from 'http';
const server = createServer();
server.on('request', (req, res) => {
console.log(`${req.method} ${req.url}`);
res.end('Hello');
});
server.on('error', (err) => {
console.error('Server error:', err.message);
});
server.listen(3000);Process signals
// Graceful shutdown on Ctrl+C
process.on('SIGINT', () => {
console.log('Shutting down...');
server.close(() => process.exit(0));
});
// Catch unhandled errors before they crash the process
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
process.exit(1);
});Avoiding memory leaks
Every listener you add is held in memory. If you keep adding listeners without removing them, inside a loop, for example, you will eventually run out of memory. Node.js logs a warning when a single event accumulates more than 10 listeners.
const emitter = new EventEmitter();
// Store a reference so you can remove it later
const onData = (data) => console.log('Data:', data);
emitter.on('data', onData);
// Clean up when you are done
emitter.off('data', onData);
// Or raise the limit if you genuinely need many listeners
emitter.setMaxListeners(25);emitter.off('data', () => {}) does nothing because it creates a new function each time. Always store a reference if you plan to remove the listener later.Naming events cleanly
Use a constants object with namespaced names so you never mistype an event string.
const Events = {
USER_CREATED: 'user:created',
USER_UPDATED: 'user:updated',
ORDER_PLACED: 'order:placed',
};
emitter.on(Events.USER_CREATED, (user) => sendWelcomeEmail(user));
emitter.on(Events.ORDER_PLACED, (order) => updateInventory(order));
// Triggering
emitter.emit(Events.USER_CREATED, newUser);