๐Ÿ’ก Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

    • Download Node.js to run JavaScript on your computer
    • Use your browser's Developer Console (Press F12) to test code snippets
    • Create a .html file with <script> tags and open it in your browser

    What You'll Learn in This Lesson

    • โœ“Observer Pattern fundamentals
    • โœ“Building a custom EventEmitter
    • โœ“on(), off(), once() methods
    • โœ“Priority listeners & namespaced events
    • โœ“Async event emitters
    • โœ“Real-world use cases (Node.js, React)

    Custom Event Emitters & The Observer Pattern

    In real JavaScript apps, you often need different parts of your code to "react" when something happens somewhere else. A user clicks a button, data finishes loading, a WebSocket receives a message, a game character takes damage, a payment succeeds, a timer ends, or an AI response arrives. You don't want everything tightly glued together with messy function calls. Instead, you want a clean, decoupled system where one part emits events and other parts subscribe and react.

    That is exactly what custom event emitters and the Observer Pattern give you: a flexible way to broadcast events and let many listeners respond without tightly coupling modules.


    What is the Observer Pattern?

    The Observer Pattern is a design pattern where:

    • You have a Subject (also called Observable or Emitter)
    • You have many Observers (also called Listeners or Subscribers)

    The Subject keeps a list of observers and notifies them when something important happens.

    In JavaScript terms:

    • The Subject is often an object with methods like .on(), .off(), .emit()
    • The Observers are callback functions you register

    ๐Ÿ’ก Simple mental model:

    "When X happens, run all the functions that are interested in X."

    This is used everywhere:

    • DOM: button.addEventListener("click", handler)
    • Node.js: emitter.on("data", handler)
    • React: custom hooks or global event buses
    • Game engines: health changes, score updates, level load events
    • Chat apps: message received, user typing, connection lost

    Building a Very Simple Custom Event Emitter

    Here's a minimal implementation you can paste into your editor:

    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(eventName, listener) {
        if (!this.events[eventName]) {
          this.events[eventName] = [];
        }
        this.events[eventName].push(listener);
      }
    
      emit(eventName, ...args) {
        const listeners = this.events[eventName];
        if (!listeners || listeners.length === 0) return;
    
        listeners.forEach(listener => listener(...args));
      }
    }
    
    // Usage example
    const emitter = new EventEmitter();
    
    emitter.on("greet", (name) => {
      console.log(`Hello, ${name}!`);
    });
    
    emitter.emit("greet", "Alice"); // Hello, Alice!

    Key ideas:

    • this.events is a map from eventName โ†’ array of callback functions
    • .on() registers a listener
    • .emit() triggers all listeners for that event

    ๐ŸŽฏ Try changing the code:

    • Add two greet listeners and see both get called
    • Create another event like "order:created"

    Why Not Just Call Functions Directly?

    Imagine a simple online store:

    • Checkout module
    • Email module
    • Analytics module
    • Inventory module

    When an order is created, you could call functions like:

    createOrder();
    sendEmail();
    trackAnalytics();
    updateInventory();

    This quickly becomes messy, tightly coupled, and painful to extend.

    With an event emitter:

    • The checkout module just emits order:created
    • Any module that cares subscribes to order:created

    Example:

    const emitter = new EventEmitter();
    
    // Email system
    emitter.on("order:created", (order) => {
      console.log("Sending email for order", order.id);
    });
    
    // Analytics
    emitter.on("order:created", (order) => {
      console.log("Tracking analytics for order", order.id);
    });
    
    // Inventory
    emitter.on("order:created", (order) => {
      console.log("Updating inventory for order", order.id);
    });
    
    function createOrder(orderData) {
      const order = { id: Date.now(), ...orderData };
      emitter.emit("order:created", order);
    }
    
    createOrder({ items: ["Apple", "Orange"], total: 9.99 });

    Now createOrder has no idea who is listening. You can add or remove observers without touching it.


    Adding .off() to Unsubscribe

    A robust event emitter lets you remove listeners too.

    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(eventName, listener) {
        if (!this.events[eventName]) {
          this.events[eventName] = [];
        }
        this.events[eventName].push(listener);
        return () => this.off(eventName, listener); // return unsubscribe fn
      }
    
      off(eventName, listener) {
        const listeners = this.events[eventName];
        if (!listeners) return;
        this.events[eventName] = listeners.filter(l => l !== listener);
      }
    
      emit(eventName, ...args) {
        const listeners = this.events[eventName];
        if (!listeners || listeners.length === 0) return;
        listeners.forEach(listener => listener(...args));
      }
    }
    
    // Example usage
    const emitter = new EventEmitter();
    
    const onLogin = (user) => console.log("User logged in:", user.name);
    
    const unsubscribe = emitter.on("login", onLogin);
    
    emitter.emit("login", { name: "Boopie" });
    unsubscribe(); // remove listener
    emitter.emit("login", { name: "AnotherUser" }); // no log

    ๐Ÿ’ก Patterns here:

    Returning an unsubscribe function from .on() is a very common pattern in modern JS (React hooks, Redux, custom hooks).

    It keeps memory usage under control and prevents old components from still responding when they're "gone".


    One-Time Events with .once()

    Sometimes you want a listener to run only once โ€” for example:

    • First time user logs in
    • First time connection succeeds
    • Only the first successful payment

    You can add a .once() helper:

    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(eventName, listener) {
        if (!this.events[eventName]) {
          this.events[eventName] = [];
        }
        this.events[eventName].push(listener);
        return () => this.off(eventName, listener);
      }
    
      once(eventName, listener) {
        const wrapper = (...args) => {
          listener(...args);
          this.off(eventName, wrapper);
        };
        return this.on(eventName, wrapper);
      }
    
      off(eventName, listener) {
        const listeners = this.events[eventName];
        if (!listeners) return;
        this.events[eventName] = listeners.filter(l => l !== listener);
      }
    
      emit(eventName, ...args) {
        const listeners = this.events[eventName];
        if (!listeners || listeners.length === 0) return;
        listeners.forEach(listener => listener(...args));
      }
    }
    
    // Example
    const emitter = new EventEmitter();
    
    emitter.once("init", () => {
      console.log("App initialized (this runs once)");
    });
    
    emitter.emit("init");
    emitter.emit("init"); // no output second time

    This is a powerful pattern when building initialization flows and onboarding experiences.


    Common Mistakes with Custom Event Emitters

    Here are very realistic bugs developers make that you should avoid:

    1. Forgetting to clean up listeners

    // In a component that is created/destroyed frequently
    emitter.on("resize", () => console.log("Resized"));
    // Never calling off() or using the unsubscribe function

    This leads to:

    • Memory leaks
    • Same handler firing 20 times later

    2. Misspelling event names

    emitter.on("order:created", handler);
    emitter.emit("order-created", data); // typo

    Nothing happens, no errors, silently fails. A good pattern is to define event name constants.

    3. Using event emitter instead of simple function calls

    Don't use an event emitter for simple internal function calls inside the same small module. It can add unnecessary complexity. Use events when you really need decoupling between parts of the app.

    4. Passing too many arguments

    If your event passes 8 parameters, it's usually better to pass a single object:

    emitter.emit("order:created", { id, items, total, userId, meta });

    Real-World Use Cases Where This Shines

    You can use custom event emitters and the Observer Pattern in:

    • Custom analytics system โ€“ emit "page:viewed", "button:clicked" and log/persist
    • Game frameworks โ€“ "player:died", "score:updated", "enemy:spawned"
    • Chat systems โ€“ "message:received", "user:joined", "typing:start"
    • Financial dashboards โ€“ "price:update", "portfolio:refresh"
    • AI tools โ€“ "job:queued", "job:completed", "token:usage:update"

    This is not "toy JavaScript." This is the architecture behind reactive, scalable apps.


    ๐Ÿ”ฅ Namespaced Events (Professional-Grade Structure)

    When building large JavaScript apps, you should avoid dumping everything into generic event names like "data" or "update". Instead, define namespaces:

    "user:login"
    "user:logout"
    "cart:item:add"
    "cart:item:remove"
    "socket:connected"
    "socket:disconnected"
    "ai:response:start"
    "ai:response:end"
    "payment:success"
    "payment:failed"

    This achieves:

    • Cleaner architecture
    • Clear mental model
    • Avoids collisions
    • Better debugging
    • Lets you group features

    Here's a small example:

    const emitter = new EventEmitter();
    
    emitter.on("cart:item:add", (item) => {
      console.log("Item added:", item);
    });
    
    emitter.emit("cart:item:add", { id: 1, name: "Apple", price: 0.99 });

    ๐ŸŽฏ Try this:

    Rename the event, add a second listener, or group multiple events under "user:*".


    ๐Ÿ”ฅ Priority Listeners (Run Important Handlers First)

    Sometimes one listener MUST run before others. Example:

    • Listener 1 โ†’ validates order
    • Listener 2 โ†’ charges payment
    • Listener 3 โ†’ sends email

    The charging listener should not run before validation.

    Add priorities:

    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(eventName, listener, priority = 0) {
        if (!this.events[eventName]) {
          this.events[eventName] = [];
        }
        this.events[eventName].push({ listener, priority });
        this.events[eventName].sort((a, b) => b.priority - a.priority);
      }
    
      emit(eventName, ...args) {
        const listeners = this.events[eventName] || [];
        for (const { listener } of listeners) {
          listener(...args);
        }
      }
    }
    
    // Example
    const emitter = new EventEmitter();
    
    emitter.on("order:process", () => console.log("โ‘ข Send email"));
    emitter.on("order:process", () => console.log("โ‘  Validate order"), 10);
    emitter.on("order:process", () => console.log("โ‘ก Charge payment"), 5);
    
    emitter.emit("order:process");

    Expected output:

    โ‘  Validate order
    โ‘ก Charge payment
    โ‘ข Send email

    This is enterprise-level architecture.


    ๐Ÿ”ฅ Wildcard Event Matching (user:* Listeners)

    Frameworks like Socket.IO and Vue.js use wildcard events.

    Example:

    emitter.on("user:*", (eventName, payload) => {
      console.log("User event detected:", eventName, payload);
    });

    Your emitter can support this:

    emit(eventName, ...args) {
      const listeners = this.events[eventName] || [];
    
      // wildcard support
      const wildcard = eventName.split(":")[0] + ":*";
      const wildcardListeners = this.events[wildcard] || [];
    
      [...listeners, ...wildcardListeners].forEach(({ listener }) =>
        listener(eventName, ...args)
      );
    }

    Now:

    emitter.emit("user:login", { name: "Alex" });

    Triggers:

    • "user:login" listeners
    • "user:*" listeners

    ๐ŸŽฏ Try this:

    Add "user:logout" and watch your wildcard handler still catch it.


    ๐Ÿ”ฅ Async Event Emitters (Wait for Listeners)

    This is used in:

    • Queue systems
    • Payment pipelines
    • Database migrations
    • AI job processing
    • Worker pools
    class AsyncEmitter {
      constructor() { this.events = {}; }
    
      on(name, fn) {
        (this.events[name] ||= []).push(fn);
      }
    
      async emit(name, ...args) {
        const fns = this.events[name] || [];
        for (const fn of fns) {
          await fn(...args);
        }
      }
    }
    
    // Example
    const e = new AsyncEmitter();
    
    e.on("task", async () => {
      console.log("Task started");
      await new Promise(res => setTimeout(res, 1000));
      console.log("Task finished");
    });
    
    (async () => {
      await e.emit("task");
      console.log("All listeners complete");
    })();

    Perfect for pipelines where stages must finish in sequence.


    ๐Ÿ”ฅ Mistakes to Avoid

    These are real bugs developers make:

    • โŒ Accidentally creating infinite loops: emitter.on("update", () => emitter.emit("update"));
    • โŒ Emitting too often (UI re-renders a thousand times)
    • โŒ Storing huge payloads (memory leaks)
    • โŒ Forgetting to off() listeners
    • โŒ Using events when simple function calls would do

    Don't overuse them โ€” use them when decoupling is necessary.


    ๐Ÿš€ Real-World Uses of Observer Pattern

    Custom event emitters aren't just "an advanced JS trick." They are the foundation of huge, real-world systems:

    โœ”๏ธ Node.js core (EventEmitter class)

    Used in:

    • Streams
    • HTTP requests
    • File system watchers
    • WebSockets

    โœ”๏ธ React & Vue (Reactivity Layer)

    Internally rely on dependency tracking systems that follow the Observer Pattern.

    A change triggers watchers โ†’ watchers update UI.

    โœ”๏ธ Stripe, PayPal, Banking Systems

    Events like:

    • payment:authorized
    • payment:captured
    • invoice:paid
    • refund:issued
    • risk:flagged

    Each event triggers multiple internal services.

    โœ”๏ธ Discord, Slack, Real-Time Chat Apps

    Real-time messaging relies on event buses for:

    • message received
    • user typing
    • user joined
    • user left

    โœ”๏ธ Game Engines

    Unity, Godot, Roblox, and JS-based engines all use event dispatchers.


    ๐Ÿงช Practice Challenges

    Challenge 1 โ€” Build a Todo Bus

    Events needed:

    • todo:add
    • todo:delete
    • todo:update
    • todo:toggle

    Students must build handlers for each.

    Challenge 2 โ€” Throttled EventEmitter

    Create an emitter that blocks repeated events if fired too fast.

    Useful for:

    • Scroll events
    • Search inputs
    • API spam protection

    Challenge 3 โ€” Record All Events

    Build an event recorder:

    [ {event: "user:login", timestamp: 17000000, payload: {...}} ]

    Let users download the log.

    Challenge 4 โ€” Async Ordered Pipeline

    Students must ensure:

    • validate
    • transform
    • save

    โ€ฆrun in order, one after another.


    Interactive Code Editor

    Try out the concepts you've learned. The editor below has examples you can run and modify:

    Event Emitter with Priority Support

    Build and test a custom event emitter with priority, once(), and namespaced events

    Try it Yourself ยป
    JavaScript
    // Basic Event Emitter with priority support
    class EventEmitter {
      constructor() {
        this.events = {};
      }
    
      on(eventName, listener, priority = 0) {
        if (!this.events[eventName]) {
          this.events[eventName] = [];
        }
        this.events[eventName].push({ listener, priority });
        this.events[eventName].sort((a, b) => b.priority - a.priority);
        
        // Return unsubscribe function
        return () => this.off(eventName, listener);
      }
    
      off(eventName, listener) {
        const listeners = th
    ...

    ๐Ÿ“Œ Final Summary

    Custom Event Emitters and the Observer Pattern are the backbone of many of the world's largest systems. By mastering event architecture โ€” namespaced events, priorities, async flows, debugging tools, memory management, pipelines, and modular design โ€” developers gain the ability to structure applications that are scalable, reactive, and easy to extend. This lesson gives you the same foundation used in React, Node.js, Stripe, Discord, and major game engines, preparing you for real-world engineering and professional JavaScript architecture.

    ๐Ÿ“‹ Quick Reference

    MethodPurpose
    .on(event, fn)Register a listener for an event
    .emit(event, ...args)Trigger all listeners for an event
    .off(event, fn)Remove a specific listener
    .once(event, fn)Listen once, then auto-unsubscribe
    "user:login"Namespaced event pattern
    AsyncEmitterAwait all listeners in sequence

    Lesson Complete!

    You've built a production-grade event emitter from scratch, implemented on/off/once/priority/async patterns, and learned the architecture behind Node.js, React, Stripe, and Discord.

    Up next: Debouncing & Throttling โ€” control high-frequency events for smooth, performant UIs.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy Policy โ€ข Terms of Service