๐ก 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
.htmlfile 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.eventsis 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 timeThis 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 functionThis leads to:
- Memory leaks
- Same handler firing 20 times later
2. Misspelling event names
emitter.on("order:created", handler);
emitter.emit("order-created", data); // typoNothing 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 emailThis 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
// 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
| Method | Purpose |
|---|---|
| .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 |
| AsyncEmitter | Await 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.