Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~27 min· topic 1 of 8

Topic 9.1

Event Listeners

In one line

An event listener is a function you register with addEventListener so the browser calls it whenever a certain event happens on an element; it receives an event object describing what happened, and removeEventListener or the once option takes it away again.

Think of it like this

A doorbell. You don't stand at the door all day waiting for visitors. You fit a bell, and when someone presses it, you hear it and go to the door. An event listener is that bell: you tell the browser "when someone clicks this button, run this function", and then your code waits without doing anything. The note the visitor leaves ("I'm the postman, I have a parcel") is the event object: details about what happened.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Event
A message from the browser that something happened, like a click, a key press or the page loading.
Event type
The short name of the event, such as "click", "keydown" or "submit".
Event listener (handler)
A function you register so the browser runs it when a certain event happens.
addEventListener
The method that attaches a listener to an element (or to document or window).
removeEventListener
The method that detaches a listener. It needs the same function you added.
Event object
The object passed to your listener, with details such as type, target and the mouse position.
target
The element where the event started, for example the button that was clicked.
isTrusted
true when a real user action caused the event, false when code created it.
Event-driven
A style of program that mostly waits, then runs code in answer to events.

Step by step

01Find the element

You need a reference to the element first, using what you learned in Topic 8.2. If the selector matches nothing, you get null and the next step throws an error.

app.jswhole filejavascript
const button = document.querySelector("#like");

02Write the listener as a named function

The function receives the event object. Giving it a name makes it easy to remove later and makes stack traces readable.

app.jswhole filejavascript
function handleLike(event) {
  console.log("Liked! The event type is", event.type);
}

03Register it

Pass the type and the function itself, without brackets. Nothing runs yet. The browser stores the pair (type, function) in the element's listener list and returns undefined.

app.jswhole filejavascript
button.addEventListener("click", handleLike);

04The user clicks: the browser calls you

When the click happens, the browser creates a PointerEvent (a kind of MouseEvent), works out the target, and calls each matching listener in the order it was added, passing the same event object to each.

The user clicks: the browser calls youdiagram
Rendering diagram…

05Fire it from code while testing

button.click() fires a click event from code, as if the user clicked. The listener runs immediately, before click() returns, and event.isTrusted is false. The live examples in this phase use this so the console shows output without you clicking.

06Remove it when you're done

Pass the same type and the same function. Or, if it should only ever run once, add it with { once: true } and the browser removes it for you.

app.jswhole filejavascript
button.removeEventListener("click", handleLike);

// or, from the start:
button.addEventListener("click", handleLike, { once: true });

Try it yourself

  1. 1

    Predict the count

    In the first example, add a third button.click(); line. Predict the last console line and the text on the page, then run it.

  2. 2

    Make the arrow removable

    In the third example, store the arrow in a constant (const onArrow = () => ...) and pass onArrow to both addEventListener and removeEventListener. Predict whether arrow listener ran still prints.

  3. 3

    Real versus code clicks

    Run the second example, then click the box yourself. Which line changes, and why?

Code & diagrams

A like button with a listener New tab

The two lines come from `button.click()`. Now click the button yourself: each real click adds another line.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

type: click | target: <button#like> | likes: 1
type: click | target: <button#like> | likes: 2
Looking inside the event object New tab

`isTrusted` is `false` because code fired this click. Click the box yourself and the same listener prints `isTrusted: true`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

type: click
target: <div#box>
currentTarget: <div#box>
this is the element? true
isTrusted: false
bubbles: true | cancelable: true
is a MouseEvent? true
Removing listeners, and once New tab

Two functions with identical code are still two different objects, so `removeEventListener` found nothing to remove in part 2.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

tick listener ran
arrow listener ran
Welcome! (shown once)
onclick replaces, addEventListener adds New tab

The `onclick` property is one slot: B replaced A. Listeners added with `addEventListener` all stay, and run in the order they were added.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

onclick B
listener 1
listener 2
same function added twice, runs once

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call the function instead of passing it

In the first example, write button.addEventListener("click", handleLike()); (with brackets).

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of undefined (reading 'type')

Break #2

Misspell the event type

Write button.addEventListener("onclick", handleLike); or "Click".

terminal
$ the page shows
── what you'll see ──
Nothing happens on click, and the console shows no error.

Break #3

Remove with a different capture setting

Add with el.addEventListener("click", fn, { capture: true }) and remove with el.removeEventListener("click", fn).

terminal
$ the page shows
── what you'll see ──
The listener keeps running after you "removed" it.

Myth vs fact

Myth

onclick and addEventListener("click") are the same thing.

Fact

onclick holds one function and a new assignment replaces the old one. addEventListener keeps a list, supports options like once and capture, and works for every event type, including custom ones.

Myth

You must remove every listener before removing an element, or memory leaks.

Fact

In modern browsers, when an element is removed and nothing else references it, it and its listeners are garbage-collected together. Leaks happen when listeners on long-lived objects (window, document) keep referencing old elements or data.

Myth

element.click() behaves exactly like a real click.

Fact

It fires a click event with isTrusted: false, and skips the pointerdown, mousedown, pointerup and mouseup events that come before a real click. Some browser features (opening popups, full screen, copying) only allow trusted events from real user actions.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Pass { signal } to remove many listeners at once: const ac = new AbortController(); el.addEventListener("click", fn, { signal: ac.signal }); ... ac.abort(); removes every listener registered with that signal. This is the cleanest teardown for components and is supported in all modern browsers (since 2021).

  • ▸

    addEventListener also accepts an object with a handleEvent(event) method instead of a function. this inside it is the object, which lets a class handle several event types in one method with a switch (event.type).

  • ▸

    Event dispatch is synchronous: el.click() or el.dispatchEvent(...) runs every listener before returning. Real user events are queued as tasks by the browser and run one at a time from the event loop (Topic 10.3). A listener that takes 200 ms freezes the page for 200 ms, which the INP (Interaction to Next Paint) Core Web Vital measures. Keep listeners short and move heavy work elsewhere.

  • ▸

    An error thrown inside one listener is reported (it appears in the console as Uncaught ...) but does not stop the other listeners for the same event. The browser catches it per listener.

Remember this

  1. 1

    An event is a signal from the browser that something happened: the user clicked (click), pressed a key (keydown), typed in a box (input), sent a form (submit), or the page finished loading (DOMContentLoaded, load). Each event has a type, a short lowercase name like "click", and a target, the element it happened to. JavaScript in the browser is event-driven: most of your code runs because an event happened, not from top to bottom once.

  2. 2

    element.addEventListener(type, listener) registers a listener (also called a handler): a function the browser will call each time an event of that type reaches the element. You pass the function itself, not the result of calling it: button.addEventListener("click", save) is right, while button.addEventListener("click", save()) calls save once immediately and registers whatever it returns. You can add many listeners for the same event on the same element; they run in the order you added them. Adding the exact same function twice for the same type does nothing the second time.

  3. 3

    When the event happens, the browser builds an event object and passes it to your listener as its first argument, usually called event or e. Useful properties: type ("click"), target (the element where the event started), currentTarget (the element whose listener is running now, see Topic 9.2), timeStamp, isTrusted (true if a real user caused it, false if code made it), and bubbles/cancelable. Specific events add more: a MouseEvent has clientX/clientY and button, a KeyboardEvent has key and code (Topic 9.5).

  4. 4

    element.removeEventListener(type, listener) removes a listener, but only if you pass the same function (the same reference in memory) and the same capture setting you used when adding it. That's why an anonymous function written inline can never be removed: writing it again creates a new, different function. Keep a name for any listener you plan to remove. Passing { once: true } as the third argument removes the listener automatically after its first run.

  5. 5

    The third argument is an options object: { once: true } (run at most once), { capture: true } (listen during the capturing phase, Topic 9.2), { passive: true } (promise not to call preventDefault, Topic 9.4) and { signal } (remove the listener when an AbortController is aborted, see the pro notes). An older form passes true instead of an object, which means { capture: true }.

  6. 6

    There is an older way too: event handler properties such as button.onclick = fn, and HTML attributes such as <button onclick="save()">. A property holds only one function, so assigning a second one silently replaces the first. Inline HTML attributes mix behaviour into the markup and are blocked by a strict Content Security Policy. Use addEventListener in new code; you'll still meet onclick in older code and tutorials. Inside a listener written with function, this is the element (event.currentTarget); inside an arrow function, this is whatever it was outside (Topic 7.1).

Explain it without notes

01

What is the difference between addEventListener and setting onclick?

02

Why can't you remove a listener that was added as an inline arrow function?

03

What does the event object contain, and how do you receive it?

04

What does isTrusted tell you, and why does it matter?

Practice

01

Make a button #hello that logs Hello! each time it is clicked. Fire it twice from code.

02

Make a #start button whose listener logs Started only the first time it is clicked, without using { once: true }.

03

Add a listener to document that logs the id of whatever element was clicked. Click a <p id="intro"> from code.

Trade-offs

  • ↔

    Inline arrow listeners are short and can use nearby variables, but can't be removed by reference; named functions or an AbortController signal take one more line but make cleanup reliable.

  • ↔

    One listener per element is easy to read for a few elements; for many similar elements, one listener on a parent (delegation, Topic 9.3) uses less memory and handles elements added later.

Done when you can

  • I can attach a listener with addEventListener and pass the function without calling it.

  • I can read type, target and isTrusted from the event object.

  • I can remove a listener with the same function reference, or use { once: true }.

  • I know why onclick replaces while addEventListener adds.

  • I can fire an event from code with element.click() to test a listener.