· 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.

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 undefined in strict mode.
  • new sets this to the newly created instance (constructor behavior).
  • call/apply/bind explicitly set this.
  • Arrow functions don’t have their own this; they inherit it lexically from the surrounding scope.
  • Top-level this differs by environment (browser global vs modules/strict undefined).

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(); // obj

Why: 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 detached

Patterns 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.

  1. Prefer explicit binding or explicit receivers

    • Call methods on the object: obj.method().
    • Use call/apply when you need to control this temporarily.
    • Use bind in constructors to permanently bind instance methods.
  2. 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 this to be the object.
  3. Prefer functions that don’t rely on this where 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 of user.greet() if you frequently detach the function.
  4. 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) or handler = () => {}.
  5. Be intentional with event listeners

    • If you need the DOM element, use a normal function to get this as the element, or use event.currentTarget which is clearer and testable.
  6. Linting and code conventions

    • Enforce a convention. For example, a linter rule to forbid use of this in modules or to require arrow functions for callbacks can reduce surprises.

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 this of 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? (this may be undefined)

Answer those and you’ll have the cause in seconds.


Resources and references


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.

Back to Blog

Related Posts

View All Posts »
Array Manipulation in a Single Line

Array Manipulation in a Single Line

Learn to transform arrays with concise, expressive one-liners using map, filter, reduce and related tools. Practical patterns: sum, unique elements, flattening, grouping, counting, and safe chaining.