cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Custom events

Create your own JavaScript events with CustomEvent, dispatch them with dispatchEvent, and decide when events are a better fit than callbacks.

By the end, you can
  • 01
    Package event dataUse CustomEvent and its detail property safely.
  • 02
    Dispatch and cancelPredict exactly when listeners run and what dispatchEvent returns.
  • 03
    Design loose connectionsChoose between custom events, callbacks, promises, and EventTarget classes.

Make one part of your program announce what happened

A custom event is an event you invent for your own app: cart:add, timer:done, profile:saved, or any other name that describes something meaningful. Instead of one object calling another object directly, the first object announces, “this happened.” Any listener that cares can react.

You already know built-in events such as click and submit from the earlier Events lessons. Custom events use the same listener system, but the event type and the data are yours. That makes them perfect when a feature should be loosely connected: a product button should not need to know about the cart badge, the analytics logger, and the discount banner.

Real-life analogyA school announcement system

Imagine a teacher announces, “Drama club meets in Room 4.” The teacher does not know every student who cares. Anyone listening can move, write it down, or ignore it. A custom event is that announcement system for your code.

In real life: Speaking into the PA
In JavaScript: Calling dispatchEvent(...)
In real life: People who chose to listen
In JavaScript: Functions registered with addEventListener
In real life: The message on the slip
In JavaScript: The detail payload
In real life: The hallway speaker also plays it
In JavaScript: bubbles: true lets ancestors hear a DOM event

Where the analogy stops: A school PA is usually one-way sound. JavaScript listeners can also cancel a cancelable event with preventDefault(), which is more like a listener pressing a stop button.

Definition

A custom event is an Event object with an app-specific type. Use CustomEvent when listeners need extra data in event.detail, and send it with target.dispatchEvent(event).

CustomEvent & detail

INTERACTIVE

The constructor shape is new CustomEvent(type, init). The type is a string. The optional init object can include detail, bubbles, cancelable, and composed. bubbles and cancelable default to false; detail defaults to null. new Event(type, init) has no detail slot.

A CustomEvent is just data plus event behaviorPop out in the code editor (opens in a new tab)JavaScript
const event = new CustomEvent("cart:add", {  detail: { id: "tea", name: "Mint tea", qty: 2 },  bubbles: true,  cancelable: true,}); console.log(event.type);console.log(event.detail.name);console.log(event.bubbles);console.log(event.cancelable);console.log(event.isTrusted);

Custom events you create in code are not user actions, so event.isTrusted is false. Choose distinctive names such as cart:add or cart-add so your app events do not sound like browser built-ins.

Try the cart. The product buttons do not update the badge directly. They dispatch a cart:add event with detail. A listener on the container updates the badge and log if the event bubbles up to it.

Cart badge event lab
Product button dispatches; container listensPop out in the code editor (opens in a new tab)JavaScript
const shop = document.querySelector("#shop");const badge = document.querySelector("#cart-count");let items = 0; shop.addEventListener("cart:add", (event) => {  items = items + event.detail.qty;  badge.textContent = String(items);  console.log("Added", event.detail.name);}); button.dispatchEvent(new CustomEvent("cart:add", {  detail: { id: "tea", name: "Mint tea", qty: 1 },  bubbles: true,  cancelable: true,}));
Live DOMbubbling
Try it yourself

Click Add. The product list does not know who is listening.

The buttons dispatch real DOM CustomEvent objects. The runtime pane is rebuilt with DOM APIs inside an empty React ref so React and manual DOM code do not fight.

dispatchEvent in motion

STEP THROUGH

dispatchEvent is not a timer and not a microtask. It runs matching listeners synchronously, in registration order, before the line after dispatchEvent continues. Its return value is usually true. It returns false only when the event is cancelable and at least one listener called preventDefault().

Step through dispatchEvent
Step 0 of 5Ready
Your turn: follow the blue line

Predict the order. Then step through the real EventTarget run recorded in Node: listeners run during dispatchEvent, not later.

Running in
  1. script
Next: line 12
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget();const log = []; target.addEventListener("status", (event) => {  log.push("listener A: " + event.detail);}); target.addEventListener("status", (event) => {  log.push("listener B");}); const ok = target.dispatchEvent(  new CustomEvent("status", { detail: "ready", cancelable: true }),);log.push("after dispatch: " + ok); console.log(log.join(" | "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the second listener

Changing this line starts a fresh recording. Predict the final return value before stepping.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
What about errors in listeners?

In browsers and in Node, an exception thrown by a listener is reported by the host rather than thrown back through the dispatchEvent call in the usual way, and other listeners can still run. Do not use thrown listener errors as a normal return channel.

That synchronous behavior is useful: a button can ask, “did anyone cancel this?” immediately. But it also means a slow listener blocks the dispatcher. Keep listeners short and move slow work to a timer, a promise, or another async API.

EventTarget in your own classes

INTERACTIVE

EventTarget is not only for DOM nodes. Modern browsers and Node 22 expose constructible, subclassable EventTarget. Extending it is like installing your own PA system in any building: a timer, a downloader, a game level, or a tiny data store can broadcast facts without importing UI code.

Real-life analogyInstalling a PA system in any building

The DOM is not the only “building” with speakers. If your own object extends EventTarget, outside code can add listeners without the object knowing who they are.

In real life: The school already has speakers
In JavaScript: DOM nodes already inherit event methods
In real life: A museum installs its own PA
In JavaScript: Your class can extend EventTarget
In real life: Many rooms can listen
In JavaScript: Many independent listeners can subscribe

Where the analogy stops: A PA system sends announcements; it does not store all past announcements. EventTarget listeners only hear events dispatched while they are registered.

Timer EventTarget lab
A class with its own event systemPop out in the code editor (opens in a new tab)JavaScript
class Timer extends EventTarget {  constructor(limit = 3) {    super();    this.count = 0;    this.limit = limit;  }   tick() {    this.count += 1;    this.dispatchEvent(new CustomEvent("tick", {      detail: { count: this.count, limit: this.limit },    }));    if (this.count === this.limit) {      this.dispatchEvent(new Event("done"));    }  }}
Timer objectstopped
0ticks heard by the display
  1. Timer ready.
Try it yourself

Two listeners hear each tick: the display updates, and the logger writes a row.

A plain JavaScript class can be an EventTarget too. It is not a DOM element; it simply supports addEventListener, removeEventListener, and dispatchEvent.

Notice the removal pattern: the logger listener was added with { signal } and disappears when the AbortController aborts. That is often cleaner than saving every listener function just to remove it later.

Events vs callbacks

COMPARE

A callback is like a phone call to one specific person: you were handed a number, and you call that number. An event is like a radio broadcast: anyone tuned to the frequency can hear it, and the broadcaster does not know who is listening.

Real-life analogyRadio broadcast vs phone call

If many parts may care, broadcast an event. If one caller needs one answer, call a callback or return a promise. Both patterns are useful; the skill is choosing the smaller coupling.

In real life: Radio station
In JavaScript: Event dispatcher
In real life: Listeners tune in or out
In JavaScript: addEventListener / removeEventListener
In real life: Phone call to one person
In JavaScript: A callback function you were handed

Where the analogy stops: Real radios cannot answer back. JavaScript events can be cancelable, but they are still best for notifications rather than asking one listener to compute a value.

One progress callbackPop out in the code editor (opens in a new tab)JavaScript
function downloadWithCallback(onProgress) {  onProgress(25);  onProgress(50);  onProgress(100);} downloadWithCallback((percent) => {  console.log("bar", percent);});
Progress event with many listenersPop out in the code editor (opens in a new tab)JavaScript
const download = new EventTarget(); download.addEventListener("progress", (event) => {  console.log("bar", event.detail.percent);}); download.addEventListener("progress", (event) => {  console.log("analytics", event.detail.percent);}); for (const percent of [25, 50, 100]) {  download.dispatchEvent(new CustomEvent("progress", {    detail: { percent },  }));}
Callbacks and events solve different coupling problems
QuestionCallback / promiseEvent
Who receives it?One function or one awaiting caller.Any listener registered for that type.
Best forComputing one result, customizing one algorithm, reporting to one owner.Broadcasting that something happened to unrelated parts.
Return channelDirect return value or resolved promise.No normal return value; maybe cancellation via preventDefault.
CouplingCaller knows the callback exists.Dispatcher only knows the event type and payload.

Event or callback?

SORTER

Sort these realistic situations. Ask: does one caller need a direct answer, or are many unrelated parts reacting to the same fact?

Event or callback?
  • Product cards, a cart badge, and an analytics panel all react to the same add-to-cart action.
  • items.sort((a, b) => a.price - b.price)
  • A modal tells its parent component that the user clicked Save.
  • One caller asks for a user profile and needs the final value or failure.
  • A timer object lets a display, a sound effect, and a logger all react to every tick.
  • names.map((name) => name.toUpperCase())
Try it yourself
0 of 6 correct

Drag each card to the pattern that keeps the code least coupled.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Where you will use custom events

Custom events show up whenever a small feature should announce a fact without importing the rest of the app.

  • Design-system widgets: a date picker dispatches date-change.
  • Web Components: a component dispatches a custom event to the page. composed: true lets an event cross a shadow DOM boundary; we will explore that later.
  • Plain modules: a downloader or timer class extends EventTarget.
  • Analytics: a feature dispatches cart:add; analytics listens without changing feature code.
Do not fake built-in default actions

Dispatching your own click event is not the same design as asking an element to do its built-in click behavior. For links and buttons in a browser, use the real DOM method such as element.click() when you need a click action. Use custom event names for your app facts.

Misconceptions and edge cases

  • “Custom events are asynchronous.” No. dispatchEvent runs listeners before returning.
  • “Every custom event bubbles.” No. bubbles defaults to false.
  • “Event and CustomEvent both have detail.” No. Use CustomEvent for payloads.
  • “dispatchEvent returns listener results.” No. It returns a cancellation boolean.
  • “Synthetic events are trusted user actions.” No. Script-created events have isTrusted === false.
  • “A custom event name should reuse click or change.” Prefer names like cart:add to avoid confusion.
Important defaults
OptionDefaultWhy it matters
detailnullListeners need CustomEvent and a detail payload when data should travel.
bubblesfalseA container will not hear a DOM event from a child unless bubbling is enabled.
cancelablefalsepreventDefault() only changes dispatchEvent's return value for cancelable events.
composedfalseShadow DOM boundaries need composed: true; that matters for Web Components later.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict synchronous dispatch

Read the starter code and predict the exact log order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget();
target.addEventListener("ping", () => console.log("listener"));
console.log("before");
target.dispatchEvent(new Event("ping"));
console.log("after");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the payload

    Name the property listeners read when a custom event carries data.

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCancellation result

      Predict the boolean returned by dispatchEvent.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const target = new EventTarget();
      target.addEventListener("save", (event) => event.preventDefault());
      const ok = target.dispatchEvent(new Event("save", { cancelable: true }));
      console.log(ok);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeBuild a tiny EventTarget

        Study the class and predict what its listener prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        class Timer extends EventTarget {
          tick() {
            this.dispatchEvent(new CustomEvent("tick", { detail: { count: 1 } }));
          }
        }
        const timer = new Timer();
        timer.addEventListener("tick", (event) => console.log(event.detail.count));
        timer.tick();

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the simpler pattern

          A profile loader has one caller waiting for one final user object. Which pattern is simpler?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Custom events quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which constructor carries custom data for listeners?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the detail snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const event = new CustomEvent("cart:add", { detail: { qty: 2 } });
              console.log(event.detail.qty);

              Choose an answer to see the explanation.

            3. Question 3 of 7What does the dispatch order snippet print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const target = new EventTarget();
              target.addEventListener("ping", () => console.log("listener"));
              console.log("before");
              target.dispatchEvent(new Event("ping"));
              console.log("after");

              Choose an answer to see the explanation.

            4. Question 4 of 7When does dispatchEvent(event) return false?

              Choose an answer to see the explanation.

            5. Question 5 of 7What is a good custom event name?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does bubbles: true change for a DOM custom event?

              Choose an answer to see the explanation.

            7. Question 7 of 7Why might a class extend EventTarget?

              Choose an answer to see the explanation.

            Key takeaways

            • CustomEvent adds detail; Event does not.
            • dispatchEvent is synchronous and returns false only for canceled cancelable events.
            • Use distinctive event names and opt into bubbles, cancelable, or composed only when needed.
            • EventTarget works in your own classes, not just DOM nodes.
            • Events broadcast facts; callbacks and promises ask one specific piece of code for a result.

            Custom events are named announcements your code dispatches so independent listeners can react to app-specific facts.

            Up next: Mouse, pointer & touch.

            CompleteFrontend Clear concepts. Working examples.