Community Article

The this Keyword: Six Rules and the Edge Cases

The six rules that fully decide the value of this in JavaScript: lexical for arrows, new, explicit binding, implicit binding, and the strict/sloppy default. Plus the four-step diagnostic I use on every this bug.

The this Keyword: Six Rules and the Edge Cases

The six rules that fully decide the value of this in JavaScript: lexical for arrows, new, explicit binding, implicit binding, and the strict/sloppy default. Plus the four-step diagnostic I use on every this bug.

js-this
js-arrow-functions
js-strict-mode
classes
interview-prep
emmadiallo

By @emmadiallo

March 7, 2026

·

Updated May 20, 2026

676 views

13

4.3 (12)

The most expensive bug I ever wrote in JavaScript was a single missing .bind(this). The codebase was a Node.js worker that processed a queue of webhooks. The class had a handleEvent method, the constructor did queue.on('message', this.handleEvent), and for three weeks the worker silently dropped errors because this.metrics was undefined inside handleEvent. Every error became Cannot read property 'increment' of undefined and crashed the message handler before the catch block ran. We finally noticed when the metrics dashboard for that queue went flat for an entire weekend.

The argument I want to make here is that this in JavaScript is not unpredictable, despite the reputation. There are exactly six rules. They have a strict precedence order. Once you know which rule applies to a given call site, the value of this is mechanical. The trouble is that most of us learned the rules by accident, in the order we tripped over them, which is why the topic feels like a swamp instead of a checklist.

What this actually refers to

this is set per function call, not per function definition. The same function, called from two different sites, can see two different this values. That is the whole reason this is confusing: it is determined by the call, not the source code. (Arrow functions are the exception, and they get their own rule below.)

When the engine evaluates a function call, it picks one of six rules to determine this for that call. The six rules, in precedence order from highest to lowest:

#RuleTriggerthis becomes
1Lexical (arrows)Function is an arrow functionInherited from the enclosing scope
2new bindingCalled with newThe freshly-constructed object
3Explicit binding.call(ctx), .apply(ctx), .bind(ctx)The provided ctx
4Implicit bindingCalled as obj.method()obj
5Default (strict)Bare call in strict modeundefined
6Default (sloppy)Bare call in sloppy modeThe global object (globalThis)

The precedence matters. If multiple rules look applicable, the higher-numbered one wins. An arrow function called with new does not get a fresh this; it just throws, because rule 1 already disqualified it. A bound function called as a method ignores the method dispatch and uses the bound context, because rule 3 outranks rule 4.

Rule 1: arrow functions inherit lexically

Arrow functions do not get their own this at call time. They inherit this from whatever scope they were defined in, the same way they inherit any other variable. This is not "the value of this from the enclosing function captured at definition time"; it is more literal than that. this inside an arrow is just a reference to this in the surrounding scope.

class Worker {
    constructor() {
        this.id = 'worker-1';
    }
    start() {
        setTimeout(() => {
            console.log(this.id);   // 'worker-1'
        }, 100);
    }
}
new Worker().start();

The arrow inside setTimeout looks up this in the enclosing function start, where this was set by rule 4 (implicit binding from worker.start()). The arrow inherits that. If we had used a regular function instead, the call setTimeout(fn, 100) is a bare call (fn() from the timer's internals), so this would have been undefined in strict mode and the global object in sloppy mode.

This single rule is why arrow functions are now the default for callbacks. The amount of var self = this and .bind(this) boilerplate that arrow functions deleted from real codebases is hard to overstate.

Rule 2: new constructs a fresh object

When you call new Fn(), four things happen in order: a new empty object is created, that object's prototype is set to Fn.prototype, Fn is called with this bound to the new object, and (if Fn does not return an object explicitly) the new object is returned.

function Point(x, y) {
    this.x = x;
    this.y = y;
}
const p = new Point(3, 4);   // this === the new {} object

new outranks every binding rule below it (except lexical, since arrows refuse to be called with new at all). You cannot override new's this with .bind. This is the one place a function's this is decided unambiguously by the call form, regardless of how the function got there.

Rule 3: explicit binding wins among "ordinary" rules

Three methods on every function let you set this directly: .call(ctx, arg1, arg2), .apply(ctx, [args]), and .bind(ctx). The first two invoke the function with the chosen this. The third returns a new function that has the binding hard-wired and ignores future attempts to change it.

function greet(prefix) {
    return `${prefix}, ${this.name}`;
}
const alice = { name: 'Alice' };
greet.call(alice, 'Hello');         // 'Hello, Alice'
greet.apply(alice, ['Hi']);         // 'Hi, Alice'
const greetAlice = greet.bind(alice);
greetAlice('Hey');                   // 'Hey, Alice'

The opening anecdote was a rule-3 omission. queue.on('message', this.handleEvent) extracts the method as a bare reference. When the queue invokes it later, the call site looks like handler(message), not worker.handleEvent(message). There is no implicit binding to recover the original receiver. The fix is queue.on('message', this.handleEvent.bind(this)), or use an arrow in the constructor (this.handleEvent = (msg) => {...}), or define the method as a class field arrow function. All three are explicit-binding solutions; the syntax differs only in whether the binding is set per-listener or per-instance.

Rule 4: implicit binding from the call site

When a function is called as a property of an object, this is set to that object.

const obj = {
    name: 'box',
    describe() {
        return this.name;
    },
};
obj.describe();   // 'box'

This is the rule most "tutorials" teach first, because it is the most intuitive. It is also the most fragile. The implicit binding lives only on the call expression; pulling the method out as a value strips the binding immediately.

const fn = obj.describe;
fn();                           // bare call: rule 5 or 6 applies
const arr = [obj.describe];
arr[0]();                       // implicit binding to arr, not obj
setTimeout(obj.describe, 0);    // bare call later: bound to nothing

The lesson is that "the method belongs to the object" is a property of the call site, not of the function. Every time a method is passed as a value (callback, setTimeout, event listener, array of handlers), the implicit binding is at risk.

Rules 5 and 6: the default binding and the strict-mode trap

A bare function call (fn(), no leading obj., no new, no .call/.apply/.bind) falls to the default rule. In strict mode, this becomes undefined. In sloppy mode, this becomes the global object (globalThis, which is window in browsers and the global object in Node).

'use strict';
function whatIsThis() {
    return this;
}
whatIsThis();   // undefined in strict, globalThis in sloppy

ES modules are always strict. Class bodies are always strict. Most modern code is strict whether you typed 'use strict' or not. That means the default binding in modern code is undefined, and any method call that loses its receiver crashes with Cannot read property 'X' of undefined rather than silently doing the wrong thing on the global object. This is a feature, not a bug; the silent-on-global behavior of sloppy mode produced bugs that were nearly impossible to find.

The edge cases

Three corners trip me up often enough to deserve their own subsection.

Methods passed as callbacks lose this. Already covered above; the fix is binding or arrow wrapping. I bring it up again because it accounts for at least half of the this-related bugs I see in code review.

Arrow functions cannot be used as methods. If you put an arrow in a class body via the legacy syntax or in an object literal, this inside the arrow points at whatever surrounded the class or object literal, not at instances of the class. For class fields specifically, arrow class fields are an exception: each instance gets its own copy of the function, with this lexically bound to the instance. That is the modern idiomatic fix for the original bind-in-constructor pattern.

class Worker {
    handleEvent = (msg) => {        // class field, arrow
        console.log(this.id);        // this is the instance
    };
}

The cost is that each instance has its own copy of the function, which is a small memory hit if you build thousands of instances. For typical app code, the safety win pays for the memory.

Strict-mode this is undefined, not null. I have watched smart engineers spend ten minutes debugging "why is this not the empty object" before realizing the value is the literal undefined. If you log this and see undefined, you have hit rule 5; you do not have a half-initialized object.

A four-step diagnostic for any this-related bug

When this is wrong, I run through four questions in this order. The first one with a "yes" is the answer.

#QuestionIf yes
1Is the function an arrow function?Look at this in the enclosing scope, not the call site
2Was it called with new?this is the freshly-constructed object
3Was it called via .call, .apply, or .bind?this is whatever was passed
4Was it called as obj.method()?this is obj

If all four answers are "no", the call is bare and this is undefined (strict) or globalThis (sloppy). The diagnostic is mechanical, the rules have strict precedence, and there is no remaining mystery.

The six rules are the whole story

Every "this is so confusing" complaint I have seen in the wild collapses into one of these six rules being misapplied or forgotten. The lexical rule for arrows handles 80 percent of modern code; the explicit-binding rule handles every callback-passing case where the lexical fix is not available; the implicit-binding rule covers ordinary method calls; new covers constructors; and the default-strict rule produces a useful crash instead of a silent global-mutation bug whenever the dispatch chain has been broken.

Memorise the rules in their precedence order, run the four-question diagnostic on the next this bug you see, and you will not be back. The opening bug from this article would have taken five minutes instead of three weeks if I had asked question four first: "is handleEvent being called as a method on a worker, or as a bare callback the queue is invoking?"

Back to Articles