Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →A small JavaScript EventEmitter needs only a map of event names to listener lists, plus methods to subscribe, publish, unsubscribe, and subscribe once. The implementation below dispatches synchronously in registration order, passes event arguments to listeners, and uses a snapshot so changes made during a dispatch affect the next one—not the dispatch already in progress. It is a teaching implementation, not a drop-in replacement for Node.js EventEmitter or the browser’s EventTarget.
What this EventEmitter does—and does not—promise
An emitter separates the code that announces something from the code that reacts to it. A publisher calls emit with an event name and optional arguments; listeners registered for that name run in turn.
As an Amazon Associate I earn from qualifying purchases.
This version uses Node-like method names and synchronous, registration-order dispatch. Its explicit mutation policy is snapshot dispatch: the listener list is copied when emit starts. Adding or removing a listener during a callback does not change which callbacks run in that current dispatch; the change applies to later emissions. Duplicate registrations are allowed, and off removes one matching registration. It intentionally omits Node-specific features, including special handling for an unhandled error event and listener-count warnings.
Build the emitter
Save the class as a JavaScript module or paste it into a script. A Map keeps each event’s listener array separate, and empty arrays are removed after unsubscription.
#1 Best Overall
class EventEmitter {
constructor() {
this.events = new Map();
}
on(eventName, listener) {
if (typeof listener !== "function") {
throw new TypeError("listener must be a function");
}
const listeners = this.events.get(eventName) ?? [];
listeners.push(listener);
this.events.set(eventName, listeners);
return this;
}
off(eventName, listener) {
const listeners = this.events.get(eventName);
if (!listeners) return this;
const index = listeners.indexOf(listener);
if (index !== -1) listeners.splice(index, 1);
if (listeners.length === 0) this.events.delete(eventName);
return this;
}
once(eventName, listener) {
if (typeof listener !== "function") {
throw new TypeError("listener must be a function");
}
let active = true;
const wrapper = (...args) => {
if (!active) return;
active = false;
this.off(eventName, wrapper);
listener(...args);
};
return this.on(eventName, wrapper);
}
emit(eventName, ...args) {
const listeners = this.events.get(eventName);
if (!listeners || listeners.length === 0) return false;
for (const listener of [...listeners]) {
listener(...args);
}
return true;
}
}
Why the methods work this way
onappends rather than replacing listeners and returns the emitter, so registrations can be chained.emitreturnstruewhen it found listeners andfalseotherwise. It invokes callbacks immediately; it does not schedule them as promises or timers.offremoves the first matching function reference. If the same function was registered twice, one call tooffremoves only one of those registrations.onceremoves its wrapper before calling the user callback. That ordering prevents a callback that emits the same event again from invoking itself a second time.- The copied array in
emitgives a stable current dispatch. For example, a listener removed by an earlier listener still runs if it was in the snapshot, while a newly added listener waits until the next emission.
Use it and check the behavior
Here is a complete example of registering, emitting, and removing listeners. The output is in registration order, and the payload is passed as the callback argument.
const emitter = new EventEmitter();
function report(message) {
console.log(`Report: ${message}`);
}
emitter.on("ready", (name) => console.log(`Ready: ${name}`));
emitter.on("ready", report);
emitter.once("ready", (name) => console.log(`First ready: ${name}`));
emitter.emit("ready", "Ada");
emitter.emit("ready", "Lin");
emitter.off("ready", report);
emitter.emit("ready", "Grace");
The first emission calls all three listeners; the second calls the two regular listeners; after off, the final emission calls only the first regular listener. The once callback is removed on its first call.
Rank #2
Test additions and removals during dispatch
The snapshot policy can be verified with this small case. The second listener still runs in the first emission even though the first listener removes it; the newly added listener starts running on the next emission.
const test = new EventEmitter();
const late = () => console.log("late");
const second = () => console.log("second");
test.on("change", () => {
console.log("first");
test.off("change", second);
test.on("change", late);
});
test.on("change", second);
test.emit("change");
// first
// second
test.emit("change");
// first
// late
The second emission’s snapshot contains the first listener and late; the removed second listener is no longer registered. Since the first listener adds late again, a further emission would contain two registrations of that same function. This implementation permits duplicates by design.
Where this differs from Node.js and browser events
The Node.js Events documentation says, “The EventEmitter calls all listeners synchronously in the order in which they were registered.” This class follows that dispatch timing, but matching that one behavior does not make it fully Node-compatible. Node also treats an emitted error specially: without an error listener, it throws the supplied error. The small class above does not implement that rule, so applications needing Node semantics should use Node’s built-in API or deliberately add and test the missing behavior. Node.js Events documentation (v22.23.1)
Node’s documentation describes a default threshold of 10 listeners for an event as a possible-memory-leak warning threshold, not a maximum that prevents more listeners from being added. This implementation has no such warning. Node.js Events documentation (v22.23.1)
Rank #4
Browser EventTarget is a related but different API: it uses addEventListener and dispatchEvent, and its listeners may be callback functions or objects with a handleEvent method. MDN also documents that a listener added while an event is being processed does not receive that same event. Do not treat the class here as a full browser event target or assume its duplicate and removal behavior is interchangeable with browser event handling. MDN: EventTarget.addEventListener()
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →When to extend this implementation
Keep the contract small and explicit. Add compatibility features only when a real caller needs them, and define their interaction with the current rules.
Quick Recap
Best Value
- For Node-compatible error behavior, make
emit("error", error)throw when no error listener exists; test both handled and unhandled cases. - If callers need to remove every copy of a duplicated listener, change
offdeliberately and document that it removes all matches rather than one. - If dispatch should stop after a listener handles an event, define a return-value or cancellation contract. This class currently calls every listener in its snapshot.
- If listeners may be asynchronous, decide whether the emitter should await them. The current
emitis synchronous and does not await promises returned by callbacks.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




