Coding problem · Medium · Design & Data Structures

Build an EventEmitter

Medium Language practicals Seen at Meta, Microsoft, Bloomberg

Implement a class EventEmitter with:

  • on(event, listener) — register a listener; return this for chaining.
  • off(event, listener) — remove that listener.
  • once(event, listener) — listener runs on the next emit only.
  • emit(event, ...args) — call listeners in registration order with args; return true if there were listeners, otherwise false.

Listeners added or removed during an emit must not affect the listeners called for that emit.

Hints

Hint 1

Store Map<event, listener[]>.

Hint 2

In emit, iterate over a copy of the array.

Hint 3

once can wrap the listener in a function that removes itself first.

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(k) per emit for k listeners · Space O(total listeners)

A map from event name to an array of listeners. emit iterates over a snapshot so listeners that unsubscribe themselves (like once) don't make the loop skip the next listener. once wraps the listener in a self-removing function and remembers the original so off(event, original) still works.

Reference solution

class EventEmitter {
  constructor() {
    this.listeners = new Map();
  }

  on(event, listener) {
    const list = this.listeners.get(event) ?? [];
    list.push(listener);
    this.listeners.set(event, list);
    return this;
  }

  off(event, listener) {
    const list = this.listeners.get(event) ?? [];
    this.listeners.set(event, list.filter((l) => l !== listener && l.original !== listener));
    return this;
  }

  once(event, listener) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      listener.apply(this, args);
    };
    wrapper.original = listener; // so off(event, listener) also removes a pending once()
    return this.on(event, wrapper);
  }

  emit(event, ...args) {
    const list = this.listeners.get(event);
    if (!list || list.length === 0) return false;
    for (const listener of [...list]) listener.apply(this, args);
    return true;
  }
}

Common mistakes

  • Mutating the array while iterating it during emit.
  • once listeners that can't be removed with off.
  • Using setTimeout — Node's emitter is synchronous.

Follow-up questions

  • Add listenerCount and a max-listeners warning (memory-leak detection).
  • Support an error event that throws when nobody listens, like Node.