October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk5 min

Build a Small JavaScript EventEmitter, Step by Step

Create a teaching-focused JavaScript EventEmitter using a Map, with on, emit, off, and once methods and explicit dispatch semantics.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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

  • on appends rather than replacing listeners and returns the emitter, so registrations can be chained.
  • emit returns true when it found listeners and false otherwise. It invokes callbacks immediately; it does not schedule them as promises or timers.
  • off removes the first matching function reference. If the same function was registered twice, one call to off removes only one of those registrations.
  • once removes 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 emit gives 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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)

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()

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

  • 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 off deliberately 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 emit is 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.