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
documentorwindow). - 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,targetand the mouse position. - target
- The element where the event started, for example the button that was clicked.
- isTrusted
truewhen a real user action caused the event,falsewhen 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.
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.
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.
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.
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.
button.removeEventListener("click", handleLike);
// or, from the start:
button.addEventListener("click", handleLike, { once: true });Try it yourself
- 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
Make the arrow removable
In the third example, store the arrow in a constant (
const onArrow = () => ...) and passonArrowto bothaddEventListenerandremoveEventListener. Predict whetherarrow listener ranstill prints. - 3
Real versus code clicks
Run the second example, then click the box yourself. Which line changes, and why?
Code & diagrams
The two lines come from `button.click()`. Now click the button yourself: each real click adds another line.
Preview
Console
Nothing logged yet.Expected console output
type: click | target: <button#like> | likes: 1
type: click | target: <button#like> | likes: 2`isTrusted` is `false` because code fired this click. Click the box yourself and the same listener prints `isTrusted: true`.
Preview
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? trueTwo functions with identical code are still two different objects, so `removeEventListener` found nothing to remove in part 2.
Preview
Console
Nothing logged yet.Expected console output
tick listener ran
arrow listener ran
Welcome! (shown once)The `onclick` property is one slot: B replaced A. Listeners added with `addEventListener` all stay, and run in the order they were added.
Preview
Console
Nothing logged yet.Expected console output
onclick B
listener 1
listener 2
same function added twice, runs onceBreak 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).
Break #2
Misspell the event type
Write button.addEventListener("onclick", handleLike); or "Click".
Break #3
Remove with a different capture setting
Add with el.addEventListener("click", fn, { capture: true }) and remove with el.removeEventListener("click", fn).
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). - ▸
addEventListeneralso accepts an object with ahandleEvent(event)method instead of a function.thisinside it is the object, which lets a class handle several event types in one method with aswitch (event.type). - ▸
Event dispatch is synchronous:
el.click()orel.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
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
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, whilebutton.addEventListener("click", save())callssaveonce 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
When the event happens, the browser builds an event object and passes it to your listener as its first argument, usually called
eventore. 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(trueif a real user caused it,falseif code made it), andbubbles/cancelable. Specific events add more: aMouseEventhasclientX/clientYandbutton, aKeyboardEventhaskeyandcode(Topic 9.5). - 4
element.removeEventListener(type, listener)removes a listener, but only if you pass the same function (the same reference in memory) and the samecapturesetting 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
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 callpreventDefault, Topic 9.4) and{ signal }(remove the listener when anAbortControlleris aborted, see the pro notes). An older form passestrueinstead of an object, which means{ capture: true }. - 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. UseaddEventListenerin new code; you'll still meetonclickin older code and tutorials. Inside a listener written withfunction,thisis the element (event.currentTarget); inside an arrow function,thisis whatever it was outside (Topic 7.1).
Explain it without notes
What is the difference between addEventListener and setting onclick?
Why can't you remove a listener that was added as an inline arrow function?
What does the event object contain, and how do you receive it?
What does isTrusted tell you, and why does it matter?
Practice
Make a button #hello that logs Hello! each time it is clicked. Fire it twice from code.
Make a #start button whose listener logs Started only the first time it is clicked, without using { once: true }.
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
AbortControllersignal 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
addEventListenerand pass the function without calling it.I can read
type,targetandisTrustedfrom the event object.I can remove a listener with the same function reference, or use
{ once: true }.I know why
onclickreplaces whileaddEventListeneradds.I can fire an event from code with
element.click()to test a listener.