Q129: Event Emitter, Pub Sub and Observer Patterns
What Interviewers Want To Evaluate
This question checks whether you understand event-driven architecture in JavaScript. Interviewers want event emitters, pub-sub, observers, listener cleanup, memory leaks, synchronous vs asynchronous dispatch, error handling, backpressure, and when events create hidden coupling.
Senior answers should connect the pattern to UI coordination, analytics, sockets, state stores, and system boundaries.
Short Interview Answer
An event emitter lets code subscribe listeners to named events and emit those events later. Pub-sub decouples publishers from subscribers, often through a broker or event bus. The observer pattern lets observers react when a subject changes. These patterns help decouple producers and consumers, but they can create hidden control flow, memory leaks, ordering problems, and debugging difficulty if ownership and cleanup are unclear.
Detailed Interview Answer
Event-driven patterns let parts of a system communicate without direct calls.
producer emits event
subscribers receive event
subscribers react independently
This is useful when many consumers care about the same signal.
Simple Event Emitter
function createEmitter() {
const listeners = new Map();
return {
on(event, listener) {
const eventListeners = listeners.get(event) ?? new Set();
eventListeners.add(listener);
listeners.set(event, eventListeners);
return () => eventListeners.delete(listener);
},
emit(event, payload) {
for (const listener of listeners.get(event) ?? []) {
listener(payload);
}
},
};
}
Returning an unsubscribe function makes cleanup explicit.
Pub Sub
Pub-sub separates publishers and subscribers through a channel or broker.
publisher -> topic -> subscribers
The publisher does not need to know who listens.
This is useful for analytics events, notification systems, plugin systems, and cross-module coordination.
Observer Pattern
In the observer pattern, observers register with a subject.
class Store {
listeners = new Set();
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
notify(state) {
for (const listener of this.listeners) {
listener(state);
}
}
}
State stores often use this model.
Synchronous Dispatch
Many emitters call listeners synchronously.
emitter.emit("saved", payload);
This means listeners run before emit returns.
Synchronous dispatch is predictable but one slow listener can delay everyone.
Asynchronous Dispatch
Async dispatch can decouple listener timing.
queueMicrotask(() => listener(payload));
This changes ordering and error handling. Use it only when the timing contract is clear.
Error Handling
If one listener throws, should other listeners still run?
There is no universal answer.
Options include:
let the error stop dispatch
catch and report listener errors
collect errors and continue
emit an error event
schedule listeners separately
The choice should be documented for shared emitters.
Memory Leaks
Listeners retain closures. If listeners are not removed, they can retain stale UI state or large data.
const unsubscribe = emitter.on("change", handleChange);
unsubscribe();
Every subscription API should make cleanup easy.
Hidden Coupling
Events can make systems hard to follow.
button emits event
analytics listens
modal listens
store listens
router listens
This may be useful, but debugging becomes harder when important behavior is far away from the trigger.
Typed Event Contracts
In TypeScript projects, event maps can make emitters safer.
type Events = {
saved: { id: string };
failed: { message: string };
};
Typed payload contracts reduce accidental event misuse.
Common Mistakes
A common mistake is using a global event bus for ordinary parent-child communication. Direct props or explicit callbacks are often clearer.
Another mistake is never removing listeners.
Another mistake is emitting broad events like "updated" without a clear payload contract.
Senior Trade-Offs
Use events when multiple independent consumers need to react to the same signal.
Avoid events when one module should call another explicitly and ordering matters.
The pattern should reduce coupling, not hide architecture.
Code or Design Example
function trackCheckoutEvents(checkout, analytics) {
return checkout.on("completed", (order) => {
analytics.track("checkout_completed", {
orderId: order.id,
total: order.total,
});
});
}
Analytics is a good event consumer because it should not control checkout flow.
Debugging Workflow
When event systems misbehave:
list all subscribers for the event
check dispatch order
check sync vs async timing
check listener errors
check unsubscribe lifecycle
check payload contract
check whether direct calls would be clearer
add instrumentation for emitted events
Interview Framing
Define emitter, pub-sub, and observer. Then discuss cleanup, error handling, timing, and hidden coupling.
Revision Notes
Event patterns decouple producers and consumers, but they require explicit ownership, cleanup, and payload contracts.
Key Takeaways
Events are powerful when used at boundaries. Inside core workflows, explicit calls are often easier to reason about.
Follow-Up Questions
- What is an event emitter?
- What is pub-sub?
- What is the observer pattern?
- Why should subscribe return unsubscribe?
- Are event listeners usually sync or async?
- What happens if a listener throws?
- How do events cause memory leaks?
- What is hidden coupling?
- When is a global event bus risky?
- How would you type event payloads?
How I Would Answer This In A Real Interview
I would explain event emitters as named subscriptions and pub-sub as publisher-to-topic-to-subscriber communication. Then I would focus on listener cleanup, dispatch timing, error handling, typed payloads, and when events hide too much control flow.