5 questions found
How do you create a custom class that emits events by extending Node's built-in EventEmitter?
Beginner
A class extends EventEmitter (from node:events) to inherit its .on(), .emit(), .once(), and .removeListener() methods, letting instances of that class emit named events at appropriate points in their logic, with any code holding a reference to that instance able to subscribe to those events without needing any other coupling to the class's internals.
const EventEmitter = require('node:events');
class Order extends EventEmitter {
complete() {
this.status = 'completed';
this.emit('completed', { orderId: this.id, timestamp: Date.now() });
}
}
const order = new Order();
order.on('completed', (data) => console.log('Order completed:', data));
order.complete();
Real-world example
An order-processing class emits a 'completed' event rather than directly calling notification and inventory-update functions from inside its complete() method, letting other parts of the application (like an analytics tracker) subscribe to that same event later without ever modifying the Order class itself.
Common follow-ups: What's the difference between .on() and .once() when subscribing to an event?;How would you check how many listeners are currently attached to a specific event on an EventEmitter instance?
Design Patterns in JavaScript;Advanced Node.js
What happens when an EventEmitter emits an event that has an 'error' listener versus one that doesn't?
Intermediate
The 'error' event is treated specially by EventEmitter -- if it's emitted and there is no registered listener for it, Node.js throws the error and, by default, crashes the process, unlike every other event type which simply does nothing (silently) if no listener is attached; this special behavior exists specifically to prevent errors from being silently swallowed and ignored, forcing every EventEmitter-based component to have explicit error handling.
const emitter = new EventEmitter();
// No listener for 'error' -- this crashes the process
emitter.emit('error', new Error('Something went wrong'));
// With a listener attached, the error is handled gracefully instead
emitter.on('error', (err) => console.error('Handled:', err.message));
emitter.emit('error', new Error('Something went wrong'));
Real-world example
A team's custom database-connection wrapper class (extending EventEmitter) crashed the entire application in production the first time a connection error occurred, because no code anywhere had attached an 'error' listener to it -- adding that listener prevented the crash and let the error be logged and handled gracefully instead.
Common follow-ups: Why did Node.js's designers choose this special-case crash behavior specifically for the 'error' event rather than every event type?;How does this relate to and differ from the unhandled promise rejection behavior discussed for async code?
Error Handling;Advanced Node.js
What is the default maximum listener limit on an EventEmitter, and why does Node.js warn when it's exceeded?
Advanced
By default, an EventEmitter instance warns (via a MaxListenersExceededWarning) if more than 10 listeners are added for the same event name -- this default limit exists specifically as a heuristic to help catch memory leaks, since accidentally adding a new listener repeatedly (like inside a loop or a request handler, without ever removing old ones) is a very common source of unbounded memory growth; the limit can be raised via setMaxListeners() when a legitimately larger number of listeners is expected and intentional.
const emitter = new EventEmitter();
emitter.setMaxListeners(20); // deliberately raising the limit for a case with many legitimate listeners
// Without raising it, adding an 11th listener triggers a warning:
// (node:12345) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
Real-world example
A team investigating a MaxListenersExceededWarning in their logs discovers it was correctly flagging a real bug -- a route handler was calling .on() on a shared EventEmitter singleton on every request without ever removing the listener, exactly the kind of leak the default warning is designed to surface.
Common follow-ups: Should raising the max listener limit ever be the actual fix, versus investigating whether it's masking a real leak?;How would you programmatically inspect exactly how many listeners are attached to diagnose a suspected leak?
Memory Management & Garbage Collection;Advanced Node.js
How does the 'once' method differ from 'on' when subscribing to an EventEmitter's events, and when would you use it?
Intermediate
.once() registers a listener that automatically removes itself after being invoked exactly one time, whereas .on() keeps a listener attached indefinitely, firing every time the event is emitted -- .once() is appropriate for events that logically only need to be handled a single time per instance (like an initial 'ready' or 'connect' event), avoiding both the need to manually remove the listener afterward and any risk of it firing again unexpectedly on a subsequent emission.
const server = net.createServer();
server.once('listening', () => {
console.log('Server is now listening (this only logs once, even if listening fires again later)');
});
server.on('connection', (socket) => {
console.log('New connection'); // fires every single time, as expected for repeated connections
});
Real-world example
A database client class uses .once('connect', ...) to run one-time initialization logic exactly when the connection is first established, while using .on('query', ...) for logging every individual query, since queries are expected to happen repeatedly throughout the connection's lifetime unlike the single initial connection event.
Common follow-ups: What would happen functionally if .once() were used for the 'connection' event on a server that expects many connections?;How would you manually remove a listener you registered with .on() once it's no longer needed?
Async Patterns;Advanced Node.js
How would you implement a custom event that carries multiple pieces of data, and what's the convention for structuring the emitted payload?
Advanced
emit() can pass any number of additional arguments after the event name, which are delivered as separate arguments to the listener function -- but the common and generally recommended convention, especially as an event's data grows more complex over time, is to emit a single structured object payload rather than several positional arguments, since adding a new piece of data later to a positional-argument approach breaks every existing listener's function signature, whereas adding a new property to an object payload doesn't.
// Fragile: adding a new positional argument later breaks existing listeners
this.emit('orderPlaced', orderId, userId, total);
// More maintainable: a single structured payload, extensible without breaking existing listeners
this.emit('orderPlaced', { orderId, userId, total, timestamp: Date.now() });
order.on('orderPlaced', (event) => {
console.log(event.orderId, event.total);
});
Real-world example
A payment-processing class initially emitted 'paymentCompleted' with three positional arguments, but after needing to add a fourth piece of data (a transaction fee) without breaking several existing listeners already deployed elsewhere in the codebase, the team refactored to emit a single object payload going forward, making future additions backward-compatible by default.
Common follow-ups: How would you version an event's payload shape if you need to make a genuinely breaking change to it eventually?;What's the tradeoff of a single generic 'event' name carrying a 'type' field inside its payload, versus many specifically-named events?
Architecture & Design Patterns;Design Patterns in JavaScript