Implement an EventEmitter with on, off, emit & once

Build an EventEmitter class from scratch. Version I uses a listener array. Version II returns a subscription object with an unsubscribe() method — the pattern used in modern event systems.

12 min read
JavaScript
Interview
Implementation
Patterns

TABLE OF CONTENTS

An EventEmitter is a pub-sub pattern with on, off, emit, and once. Version I uses a listener array per event. Version II returns a subscription object with unsubscribe() — the modern pattern. The interviewer wants to see data structure choice and that you think about emit-during-emit.

Related deep-dive: Patterns #1 — Event Emitter / Pub-Sub


What is an EventEmitter?

An EventEmitter is the publish-subscribe (pub-sub) pattern implemented as a class with four core methods: on, off, emit, and once. It lets objects communicate without directly referencing each other — listeners register interest in named events, and emitters broadcast events to whoever is listening.

This is the decoupling pattern that powers nearly every event-driven system:

  • DOM eventselement.addEventListener("click", handler) is the same pattern
  • Node.js coreEventEmitter is a built-in module; streams, servers, and process objects all extend it
  • State management — Redux, MobX, and Zustand all use pub-sub internally to notify subscribers of state changes
  • Micro-frontends / plugins — independent modules communicate through a shared event bus without importing each other

The data structure is straightforward: a map from event name to an array of listener callbacks. The subtle behaviors that interviewers test:

  • on(event, listener) — registers a listener; multiple listeners for the same event are called in registration order
  • emit(event, ...args) — synchronously calls each registered listener with the provided arguments
  • off(event, listener) — removes a specific listener; removing during emit shouldn't skip other listeners
  • once(event, listener) — registers a listener that auto-removes after its first invocation (implemented by wrapping the listener with on + self-removing wrapper)

The upgrade path to version II is returning a subscription object with an unsubscribe() method instead of using off(event, listener). This is the modern pattern (RxJS, React's useEffect cleanup) because the subscriber doesn't need to remember both the event name and the listener reference — they just call .unsubscribe().


The Problem

"Implement an EventEmitter class with four methods: on(event, listener), off(event, listener), emit(event, ...args), and once(event, listener)."

"Version II: on returns a subscription object { unsubscribe() } instead of using off."


Thought Process

The data structure is a Map<eventName, listener[]>:

  • on — push the listener to the array (create if needed)
  • off — find and remove the listener from the array (reference equality)
  • emit — iterate the array and call each listener with the args
  • once — wrap the listener in a function that removes itself after the first call

For Version II, on returns { unsubscribe() { emitter.off(event, listener) } } — the listener manages its own lifecycle.


Step 1 — Base EventEmitter

Loading editor...


Step 2 — emit-during-emit Safety

The interviewer says: "What if a listener removes itself (or others) during emit?"

Loading editor...

The fix: [...listeners] creates a shallow copy. Mutations during emit don't affect the iteration.


Step 3 — Version II: Subscription Object

Loading editor...


Step 4 — Edge Cases

Emit with no listeners: Return false (or undefined). Don't throw.

Remove non-existent listener: Silently return — same as native EventTarget.removeEventListener.

Same listener added twice: Both registrations fire on emit. off removes only the first match.

Once listener removed manually: The wrapper is stored in the array. Calling off with the original listener won't find it — the wrapper is a different reference. This is expected behavior.


Full Solution

Loading editor...


What Interviewers Are Testing

  • Pub-sub pattern — the core observer design pattern
  • Data structure choice — Map of arrays for O(1) event lookup
  • emit-during-emit safety — iterating a copy to avoid mutation issues
  • Subscription pattern — returning a handle that controls listener lifecycle
  • Closure for once — wrapping a listener with self-removal logic

Complexity

TimeSpace
onO(1)O(1)
offO(L) — L listeners per eventO(1)
emitO(L) — iterates all listenersO(L) — copy of listeners
onceO(1)O(1)

Interview Tips

  • Choose Map over object — "I'll use a Map so I can emit events named after Object.prototype properties like toString or constructor without collision."
  • Copy before iterating in emit[...listeners] prevents bugs when a listener removes itself during emission.
  • Show Version II naturally — "Many modern event systems return a subscription object instead of using off. Here's that version."
  • Mention EventTarget and EventEmitter — the browser uses EventTarget (addEventListener/removeEventListener); Node.js uses EventEmitter. Yours is the Node.js style.

Related Questions


Let's Connect

© 2026 Naveen Karthik // Built with React & MUI