Skip to content
exercises
JavaScript

Tiny event emitter

on, off, emit. Nothing else.

BasicCourse

Implement a minimal event emitter with on, off, and emit methods. This is the pattern behind Node’s EventEmitter, browser EventTarget, and most state management libraries.

The Implementation

class EventEmitter {
#listeners = new Map();
on(event, fn) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(fn);
return () => this.off(event, fn);
}
off(event, fn) {
this.#listeners.get(event)?.delete(fn);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach((fn) => fn(...args));
}
}

on returns an unsubscribe function — a common pattern that avoids having to hold a reference to fn just to call off later.

Usage

const emitter = new EventEmitter();
const unsubscribe = emitter.on("message", (text) => {
console.log("received:", text);
});
emitter.emit("message", "hello"); // received: hello
emitter.emit("message", "world"); // received: world
unsubscribe();
emitter.emit("message", "gone"); // nothing

Why Set Instead of Array

A Set prevents duplicate listeners automatically. If someone calls on('event', fn) twice with the same function, it’s registered once. With an array you’d need a manual deduplication check.

Once Variant

once(event, fn) {
const wrapper = (...args) => {
fn(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}

Wraps the listener in a function that removes itself after the first call.

Why It Matters

The pub/sub pattern decouples the thing that fires events from the things that respond to them. Once you understand EventEmitter, you understand how DOM events, WebSockets, and most reactive libraries work under the hood.