Why this Changes in JavaScript Event Handlers and Methods

In Brief
For an ordinary JavaScript function, this is determined by how the function is called; an arrow function instead closes over this from its surrounding scope. In a DOM listener, event.currentTarget is often clearer than relying on this. Use bind() when a callback genuinely needs a fixed receiver, not as a reflex for every method passed elsewhere.
The JavaScript this keyword has been confusing people for years for one very ordinary reason: it looks like it should point to the object a function "belongs" to, but that is not actually how it works.
That assumption is where the trouble starts. New developers see a function living on an object and quite reasonably think this inside that function will always mean that object. Sometimes it does. Sometimes it does not. The deciding factor is not where the function was written. It is how the function is called.
That is the rule that makes this feel slippery until it clicks.
this is set by the call site
For an ordinary, unbound function, the call determines this. Arrow functions are the exception discussed below: they keep this from the scope in which they were created.
Look at a simple method call:
'use strict';
const cart = {
total: 99,
printTotal(): void {
console.log(this.total);
},
};
cart.printTotal();Here, this inside printTotal points to cart because the function is being called as a method of cart.
That feels natural enough. The trouble appears when the same function is called differently.
const print = cart.printTotal;
print();The method is now detached from cart. In strict mode, this plain call gives it this as undefined, so reading this.total throws a TypeError. In non‑strict mode, an ordinary plain call substitutes the global object instead; that still does not restore cart. The method in the preceding example is explicitly strict.
That is why this bugs often show up when a method is passed around as a callback rather than called directly where it lives.
Methods Work until You Pass Them Elsewhere
This is the classic trap.
const timer = {
seconds: 0,
tick(): void {
this.seconds += 1;
},
};
setTimeout(timer.tick, 1000);Many beginners expect that to increment timer.seconds. It does not, because setTimeout is not calling the function as timer.tick(). It is calling the function later as a callback. The original method context has been lost.
That is one of the most useful ways to think about this: it is fragile when functions are passed around.
Event Handlers Add Another Layer of Confusion
DOM event handlers make this feel even stranger because there are two very tempting candidates:
- the object where you wrote the handler
- the element that fired the event
In a normal DOM event listener, this inside a regular function callback refers to the element the listener is attached to.
const button = document.querySelector<HTMLButtonElement>('.save-button');
button?.addEventListener('click', function () {
console.log(this.textContent);
});In that example, this refers to the button element.
That can be useful, but it also means this inside an event listener is not some magical reference back to the module, component, or object where you defined the handler. It is tied to the way the browser invokes that function.
If you want the element more explicitly, event.currentTarget is often clearer:
button?.addEventListener('click', (event) => {
const currentTarget = event.currentTarget as HTMLButtonElement;
console.log(currentTarget.textContent);
});That tends to be easier to read than relying on this, especially once callbacks become more nested.
Arrow Functions Change the Picture
Arrow functions do not have their own this. Instead, they close over the surrounding this from the place where they were created.
That sounds abstract until you compare the behaviour directly.
const cart = {
total: 99,
printLater(): void {
setTimeout(() => {
console.log(this.total);
}, 1000);
},
};Because the arrow function does not create a new this, it uses the this from printLater, which in this case is cart.
That makes arrow functions a practical fix for many callback‑related this bugs. The trade‑off is that they do not behave like regular functions, which is sometimes exactly what you want and sometimes not.
The important part is not to think "arrow functions make this easier" in some vague way. The precise reason they help is that they do not rebind this at the callback boundary.
bind exists for exactly this problem
Another common fix is bind.
const timer = {
seconds: 0,
tick(): void {
this.seconds += 1;
},
};
setTimeout(timer.tick.bind(timer), 1000);bind() creates a new function that uses the supplied receiver for ordinary calls to an ordinary target function. Binding an arrow cannot replace its lexical this. If the target is constructible and you call the bound function with new, construction uses the new instance and ignores the bound receiver.
That is useful when:
- you want to pass a method as a callback
- you need a stable function reference with a fixed context
- an API is going to call your function later in a way you do not control
Each call to bind() creates a different function object. If you bind a method for addEventListener(), store that returned function and pass the same reference to removeEventListener() later. Calling bind() again during removal will not identify the listener you added.
The bug is usually not this itself
Most of the time the real bug is not that JavaScript changed the rules halfway through. The rules were consistent. Our mental model was wrong.
If you read code like this:
const player = {
name: 'Ellie',
announce(): void {
console.log(this.name);
},
};
const handler = player.announce;
handler();the important question is not "why did JavaScript betray me?" It is "how is handler being called?" Once the answer is "as a plain function", the changed value of this stops feeling random.
In event code, this is often less clear than the event object
This is not a moral rule, but it is a useful one.
When you are handling browser events, code is often easier to read if you work with the event object rather than relying on this.
For example:
button?.addEventListener('click', (event) => {
const target = event.currentTarget as HTMLButtonElement;
target.disabled = true;
});That says more plainly what is happening. You are taking the current target of the event and updating it. There is no extra context puzzle for the next developer to mentally resolve.
That is especially helpful when the surrounding code already uses objects, classes, or closures that all have their own possible meanings for this.
Sometimes the best fix is to avoid this
This is the part that newer developers often do not hear early enough.
You do not always need this at all.
If a function can take explicit arguments instead of depending on call‑site context, that is often easier to test and easier to reason about.
const printTotal = (total: number): void => {
console.log(total);
};
printTotal(99);That is not always the right design, but it is worth considering. Many this problems disappear once the code stops depending on implicit context and starts using explicit data flow instead.
Wrapping Up
For ordinary functions, start with the call site: a method call, a detached call and a constructor call supply different receivers. Then check whether the function is bound or is an arrow with lexical this. Those details explain the result more reliably than the object on which you happened to find the function.
Key Takeaways
- For an ordinary unbound function, the call determines
this. - Methods lose their original context easily when passed around as callbacks.
- In DOM event listeners, regular function callbacks usually get
thisbound to the current element. - Arrow functions do not create their own
this; they inherit it from the surrounding scope. bind()fixes the receiver for ordinary calls to ordinary functions; arrows and construction follow the exceptions above.
Once you stop thinking of this as ownership and start thinking of it as call‑site context, a lot of awkward JavaScript suddenly starts making much more sense.
Postscript
November 2019: The event‑listener examples now shown use optional chaining (?.), introduced in TypeScript 3.7. That syntax came after this article's July 2015 publication. It shortens the checks for a missing DOM element; the explanation of how this and bind() behave is unchanged.