August 3, 2026
The JavaScript Bug That Taught Me How ‘this’ Actually Works
It took a broken button click and an hour of confusion to finally get it

By Babar saad
3 min read
For a long time, I "knew" how this worked in JavaScript the way you know a fact for a quiz — I could recite the rule, but I didn't actually feel it. Then a bug forced me to understand it for real, and I haven't forgotten it since.
The Bug That Started It All
I was building a simple counter using plain JavaScript — no React, no Vue, just a class with a button that should increase a counter every time it was clicked. Here's what I wrote:
javascript
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(`Count is now ${this.count}`);
}
attachTo(button) {
button.addEventListener("click", this.increment);
}
}
const counter = new Counter();
counter.attachTo(document.querySelector("#my-button"));class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(`Count is now ${this.count}`);
}
attachTo(button) {
button.addEventListener("click", this.increment);
}
}
const counter = new Counter();
counter.attachTo(document.querySelector("#my-button"));It looked completely reasonable. I clicked the button expecting Count is now 1. Instead, I got:
Uncaught TypeError: Cannot read properties of undefined (reading 'count')Uncaught TypeError: Cannot read properties of undefined (reading 'count')The First Thing I Checked
Whenever JavaScript surprises me now, I do one simple thing — log this right where it's confusing me:
javascript
increment() {
console.log(this);
this.count++;
}increment() {
console.log(this);
this.count++;
}I clicked the button again. Instead of logging my Counter instance, it logged the button element itself. That was the moment everything started making sense.
Why Did This Happen?
The problem was this line:
javascript
button.addEventListener("click", this.increment);button.addEventListener("click", this.increment);It looks like you're passing the increment() method from your object. You're not — you're passing only the function. JavaScript doesn't permanently attach a function to the object it came from. Instead, this is determined when the function is called, not when it's created.
When the browser handles the click event, it effectively does something similar to this:
javascript
handler.call(button, event);handler.call(button, event);Which means inside increment():
javascript
console.log(this === button); // true
console.log(this === counter); // falseconsole.log(this === button); // true
console.log(this === counter); // falseSince the button doesn't have a count property, JavaScript throws an error.
Fix #1 — Use bind()
The most traditional solution is to bind the method:
javascript
button.addEventListener("click", this.increment.bind(this));button.addEventListener("click", this.increment.bind(this));bind() creates a new function whose this is permanently set to the Counter instance. No matter who calls it later, this will always be correct.
Pros: very explicit, works everywhere, easy to understand. Cons: creates a new function, can get repetitive if many methods need binding.
Fix #2 — Wrap It in an Arrow Function
Another common approach is using an arrow function:
javascript
button.addEventListener("click", () => {
this.increment();
});button.addEventListener("click", () => {
this.increment();
});Arrow functions don't create their own this — they inherit it from the surrounding scope. Since the surrounding scope is the Counter instance, this stays exactly where you expect it.
Pros: short and readable, no explicit binding. Cons: creates a wrapper function every time you attach the listener.
Fix #3 — Make the Method an Arrow Function
This is the solution I ended up using:
javascript
class Counter {
count = 0;
increment = () => {
this.count++;
console.log(`Count is now ${this.count}`);
};
attachTo(button) {
button.addEventListener("click", this.increment);
}
}class Counter {
count = 0;
increment = () => {
this.count++;
console.log(`Count is now ${this.count}`);
};
attachTo(button) {
button.addEventListener("click", this.increment);
}
}Because increment is itself an arrow function, it captures this when the object is created. Now I can safely pass it around anywhere:
javascript
button.addEventListener("click", this.increment);
setTimeout(this.increment, 1000);
Promise.resolve().then(this.increment);button.addEventListener("click", this.increment);
setTimeout(this.increment, 1000);
Promise.resolve().then(this.increment);No extra binding. No wrapper functions. No surprises.
A Mental Model That Finally Worked for Me
Instead of thinking "methods belong to objects," try this instead:
Objects own functions. Functions do not permanently own objects.
The object can hand the function to someone else. Once that happens, whoever calls the function decides what this becomes. That simple shift in thinking made JavaScript's behavior much easier to predict.
Where This Bug Shows Up Again
Once I understood this, I started noticing the same issue everywhere — in setTimeout():
javascript
setTimeout(this.increment, 1000);setTimeout(this.increment, 1000);in promises:
javascript
fetch("/api").then(this.increment);fetch("/api").then(this.increment);and in array callbacks:
javascript
items.map(this.formatItem);items.map(this.formatItem);Any API that stores your callback and invokes it later can trigger this exact bug. Whenever you pass a method as a callback, ask yourself one question: who is actually going to call this function?
The Takeaway
That question alone has saved me from countless debugging sessions since. If you've been getting tripped up by this in your own code, don't just re-read the explanation one more time — go find a method you're passing as a callback somewhere in your codebase and check what it's actually bound to. You'll probably find at least one place this bug is quietly waiting to happen. I did.
Before you go
- Please take a moment to like the post and follow the writer!
- Did you know that over 400,000 developers share what they're building, learning, and discovering across our platforms every month? Learn how you can contribute here