· tips · 5 min read
Surprising Side Effects: Understanding JavaScript's 'this' Keyword
Learn to predict and manage the surprising values of JavaScript's 'this'. Practical examples show common pitfalls (detached methods, setTimeout, event handlers) and clear strategies (bind, arrow functions, closures) to make 'this' reliable.

Why this matters - and what you’ll get out of this article
You’ll learn to predict the value of this in real code. No guesswork. No mysterious bugs that only show up in production. Read on and you’ll spot the cause the moment someone says “my method lost its context.”
JavaScript’s this is powerful - and famously unintuitive. It changes value depending on how a function is called, and that leads to subtle bugs for newcomers (and sometimes for seasoned developers). Below are the rules, real examples of common mishaps, and pragmatic strategies to make this behave.
The short outcome-first rules
- A function called as a method receives the object before the dot as
this. - A plain function call gets the global object in non‑strict mode, and
undefinedin strict mode. newsetsthisto the newly created instance (constructor behavior).call/apply/bindexplicitly setthis.- Arrow functions don’t have their own
this; they inherit it lexically from the surrounding scope. - Top-level
thisdiffers by environment (browser global vs modules/strictundefined).
If you remember those rules, you’ll catch 90% of surprises.
(If you want a deep reference, MDN’s this article is excellent: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)
Concrete examples and common gotchas
I’ll show minimal examples where this surprises people, explain why, and show how to fix it.
1) Plain function call vs method call
function show() {
console.log(this);
}
const obj = { show };
show(); // non-strict: window (or global), strict: undefined
obj.show(); // objWhy: A function called as a property access (obj.show()) binds this to obj. A plain show() call doesn’t.
2) Detached method: common bug
const user = {
name: 'Ava',
greet() {
console.log(this.name);
},
};
const greet = user.greet;
greet(); // undefined (or error in strict mode)Why: greet is called as a plain function, not as user.greet() - this is lost.
Fixes:
- Call with the object:
user.greet() - Bind the function:
const greet = user.greet.bind(user) - Use an arrow in the wrapper:
const greet = () => user.greet()
3) setTimeout and callbacks
const o = {
value: 42,
logLater() {
setTimeout(function () {
console.log(this.value);
}, 100);
},
};
o.logLater(); // undefined - `this` in the callback is not `o`Fixes:
- Arrow callback to inherit
this:setTimeout(() => console.log(this.value), 100) - Bind the function:
setTimeout(function() {...}.bind(this), 100)
4) DOM event handlers
<button id="btn">Click</button>
<script>
document.getElementById('btn').addEventListener('click', function (event) {
console.log(this); // <button id="btn"> - the element
console.log(event.currentTarget === this); // true
});
</script>Note: With event listeners, this is typically the element the handler is attached to. If you use an arrow function there, this will be lexically inherited (probably not the element), so choose intentionally.
5) Constructor (new)
function Thing(name) {
this.name = name;
}
const t = new Thing('x');
console.log(t.name); // 'x'Using new makes this point to the new object. Omit new and this may be global or undefined (strict), so prefer class/factory patterns.
6) Arrow functions: lexical this
const obj = {
id: 1,
regular() {
console.log(this.id);
},
arrow: () => {
console.log(this.id);
},
};
obj.regular(); // 1
obj.arrow(); // undefined (arrow captured outer `this` - often window or module `undefined`)Arrow functions don’t get their own this. That’s great for callbacks that should preserve the surrounding this, and disastrous when you expect method calls to bind to the owning object.
7) call / apply / bind
function show() {
console.log(this.name);
}
const alice = { name: 'Alice' };
show.call(alice); // 'Alice'
show.apply(alice); // 'Alice'
const bound = show.bind(alice);
bound(); // 'Alice'call/apply invoke the function with a chosen this. bind returns a new function permanently bound to the given this.
8) Classes and React-style pitfalls
ES6 classes behave like constructors: methods are not auto-bound to instances.
class Counter {
constructor() {
this.count = 0;
}
inc() {
this.count++;
}
}
const c = new Counter();
const inc = c.inc;
inc(); // TypeError or `this` undefined - method detachedPatterns to fix in class-based code:
- Bind in constructor:
this.inc = this.inc.bind(this) - Use class fields with arrow methods:
inc = () => { this.count++; }(stage 3 / widely supported in transpiled environments)
9) Module and top-level this
In ES modules, top-level this is undefined. In browsers with scripts not as modules, top-level this is window. Node CommonJS assigns this to module.exports at the top-level. So the top-level value depends on environment and module format. See MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#top_level_this
Practical strategies to avoid surprises (pick one or two consistent rules)
These are pragmatic and battle-tested.
Prefer explicit binding or explicit receivers
- Call methods on the object:
obj.method(). - Use
call/applywhen you need to controlthistemporarily. - Use
bindin constructors to permanently bind instance methods.
- Call methods on the object:
Use arrow functions for callbacks that should inherit
this- For timers, promise callbacks, shorter event handler wrappers:
setTimeout(() => this.x, 100). - Don’t use arrow functions as object methods if you expect
thisto be the object.
- For timers, promise callbacks, shorter event handler wrappers:
Prefer functions that don’t rely on
thiswhere possible- Use closures, factory functions, or pass the object explicitly as the first argument. This removes ambiguity.
- Example:
function greet(user) { console.log(user.name); }instead ofuser.greet()if you frequently detach the function.
Bind class methods predictably
- Bind in constructor or use class-field arrow syntax to ensure deterministic behavior in callbacks:
this.handler = this.handler.bind(this)orhandler = () => {}.
- Bind in constructor or use class-field arrow syntax to ensure deterministic behavior in callbacks:
Be intentional with event listeners
- If you need the DOM element, use a normal function to get
thisas the element, or useevent.currentTargetwhich is clearer and testable.
- If you need the DOM element, use a normal function to get
Linting and code conventions
- Enforce a convention. For example, a linter rule to forbid use of
thisin modules or to require arrow functions for callbacks can reduce surprises.
- Enforce a convention. For example, a linter rule to forbid use of
Quick debugging checklist when this behaves badly
- How was the function called? (method vs plain function vs constructor vs bound)
- Is the function an arrow function? (no
thisof its own) - Is the function passed as a callback and invoked by someone else? (likely lost context)
- Are you in strict mode or an ES module? (
thismay beundefined)
Answer those and you’ll have the cause in seconds.
Resources and references
- MDN - this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
- MDN - Arrow functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- MDN - Function.prototype.bind: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind
In short: this is not magical - it’s contextual. Once you master the call-site rules and choose a consistent strategy (explicit binding, arrow functions where appropriate, or avoiding this entirely), the surprises stop. Make the context explicit and the language will do as you expect.



