iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
Build a small JavaScript event emitter with a Map of event names to listener arrays, plus on, off, once, and emit methods. This tutorial defines a compact, synchronous, Node-inspired teaching API—not a drop-in replacement for Node.js EventEmitter or the browser’s EventTarget.
What this event emitter does
An event emitter lets one part of a program register callbacks for named events and another part announce those events. The emitter stores listeners by event name; when code emits an event, it invokes the listeners and passes along any supplied arguments.
The implementation below makes its contract explicit:
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problems- Listeners run synchronously, in registration order.
- The current dispatch uses a snapshot: adding or removing listeners during a callback affects later emissions, not the listener list already being dispatched.
- Registering the same function more than once creates multiple registrations.
offremoves only the first matching registration.onceremoves its own registration before calling the original listener.- An unhandled
errorevent is not treated specially; this is one important difference from Node.js.
Implement the emitter
Save this class in a JavaScript file or paste it into a browser console or Node.js project. It uses standard JavaScript features and does not depend on a particular framework.
#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");
}
const wrapped = (...args) => {
this.off(eventName, wrapped);
listener.apply(this, args);
};
return this.on(eventName, wrapped);
}
emit(eventName, ...args) {
const listeners = this.events.get(eventName);
if (!listeners || listeners.length === 0) return false;
for (const listener of [...listeners]) {
listener.apply(this, args);
}
return true;
}
}
How the methods work
on: register a listener
on retrieves the array for an event name, creates one if needed, appends the callback, and returns the emitter so calls can be chained. The type check catches a common mistake early: attempting to register something that cannot be called.
emit: dispatch an event
emit looks up the listeners and calls each one immediately with the extra arguments supplied to emit. It returns true if there was at least one listener and false otherwise. The spread copy, [...listeners], establishes the snapshot policy: changes to the stored list during dispatch do not change which callbacks are in the current iteration.
Rank #2
For example:
const events = new EventEmitter();
events.on("message", (text, sender) => {
console.log(`${sender}: ${text}`);
});
events.emit("message", "Build finished", "CI");
// CI: Build finished
off: remove a registration
off uses function identity to find a callback. Keep a reference to the same function when you need to remove it; a newly written arrow function with identical code is still a different function object. Because duplicates are permitted, this implementation removes one matching registration per call.
function report(value) {
console.log(value);
}
events.on("status", report);
events.off("status", report);
once: run a listener one time
once installs a wrapper that unregisters itself before calling the original callback. That ordering matters if the callback synchronously emits the same event again: the wrapper is already gone, so it cannot fire a second time.
let calls = 0;
events.once("ready", () => {
calls += 1;
events.emit("ready");
});
events.emit("ready");
console.log(calls); // 1
Check dispatch and removal behavior
These examples make the snapshot and duplicate-registration decisions observable. They are useful as small tests when adapting the emitter.
Adding a listener during dispatch
const events = new EventEmitter();
const seen = [];
function late() {
seen.push("late");
}
events.on("tick", () => {
seen.push("first");
events.on("tick", late);
});
events.on("tick", () => seen.push("second"));
events.emit("tick");
console.log(seen); // ["first", "second"]
seen.length = 0;
events.emit("tick");
console.log(seen); // ["first", "second", "late"]
The newly added listener is stored immediately, but it is not in the snapshot for the first emission. It participates in the next emission.
Rank #4
Removing a listener during dispatch
const events = new EventEmitter();
const seen = [];
function second() {
seen.push("second");
}
events.on("tick", () => {
seen.push("first");
events.off("tick", second);
});
events.on("tick", second);
events.emit("tick");
console.log(seen); // ["first", "second"]
seen.length = 0;
events.emit("tick");
console.log(seen); // ["first"]
Removing a callback changes the stored listener list but does not cancel its turn in the current snapshot. If you want removal to stop a callback that has not run yet, use a different dispatch policy and document it; that is not the behavior of this implementation.
Free tools Windows power users keep installed
One-click scans. No signup required.
How this differs from Node.js and the browser
Related event APIs have different contracts. Node.js documents that its EventEmitter calls listeners synchronously in registration order. Its API also gives unhandled error events special treatment, and documents a default warning threshold of 10 listeners per event; that threshold warns about possible memory leaks but does not prevent more listeners from being added. See the Node.js Events documentation.
Best Value
The implementation here follows synchronous ordered dispatch but is deliberately smaller. It does not reproduce the full Node.js API, including special unhandled-error behavior or listener warnings. The Node.js documentation surfaced for this article identifies version 22.23.1; check the documentation for the specific runtime you target if exact compatibility matters.
Browser EventTarget uses addEventListener() and dispatchEvent(), with its own event and listener rules. A listener can be a callback function or an object with a handleEvent method. MDN also notes that a listener added from inside another listener while an event is being processed does not receive that same event. Those details do not make a callback-based emitter an EventTarget implementation. See MDN’s addEventListener reference.
Quick Recap
When to extend this implementation
- Need Node.js compatibility? Implement the documented error-event behavior and any additional Node APIs your application relies on, then test against the target Node.js version. Do not treat this small class as a substitute for the built-in module.
- Need browser event compatibility? Use
EventTargetor implement its event-object and listener contract rather than only copying the method names. - Need listener diagnostics? You can add an optional warning when a per-event count becomes large. Node.js’s documented default of 10 is a warning threshold in that runtime, not a universal safe limit or a cap.
- Need different mutation behavior? Choose whether dispatch snapshots the list or reads it live, and test additions, removals, duplicates, and nested emissions against that choice.
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.

