this Binding: call, apply, bind, and Arrow Functions
The this bug I see most often in interviews is not esoteric. It is this: a candidate writes a clean class, passes a method as a callback, and the method silently crashes because this is undefined. They stare at it for a few minutes and cannot explain why. This lesson explains exactly why, and gives you the mental model to never be surprised by this again.
The Core Rule: this Is Determined at Call Time
this is not a variable that you declare. It is a binding that is determined at the moment a function is called, not when it is defined. The only exception is arrow functions, which we will cover last.
There are four rules. They are applied in priority order.
Rule 1: Default Binding
When a function is called as a plain function, not as a method and not with new, this defaults to the global object in sloppy mode or undefined in strict mode. TypeScript compiles with strict mode by default, so you will see undefined.
function greet(): void {
// In strict mode, this is undefined here
console.log(this);
}
greet(); // undefined (strict mode)
This is the "nothing matched" case. It almost always indicates a bug.
Rule 2: Implicit Binding
When you call a function as a method, this is the object to the left of the dot.
const user = {
name: "Alice",
greet(): void {
console.log(`Hello, ${this.name}`);
},
};
user.greet(); // Hello, Alice
The binding is implicit because there is nothing explicit in the call. The engine infers this from the call site. The function itself has not changed. Only the call site matters.
Here is where people get burned. If you assign the method to a variable and then call it, the object context is lost.
const fn = user.greet;
fn(); // Hello, undefined: this is now undefined (strict mode)
You separated the function from the object. The function is now called as a plain function, so rule 1 applies.
Rule 3: Explicit Binding: call, apply, bind
You can force this to be whatever you want using call, apply, or bind.
call invokes the function immediately with a given this and individual arguments.
apply invokes the function immediately with a given this and arguments as an array.
bind returns a new function with this permanently bound. The new function can be called later.
function introduce(greeting: string, punctuation: string): void {
console.log(`${greeting}, I am ${this.name}${punctuation}`);
}
const person = { name: "Bob" };
// call: arguments listed individually
introduce.call(person, "Hi", "!"); // Hi, I am Bob!
// apply: arguments in an array
introduce.apply(person, ["Hey", "..."]); // Hey, I am Bob...
// bind: returns a new function, call it later
const boundIntroduce = introduce.bind(person, "Hello");
boundIntroduce("?"); // Hello, I am Bob?
bind is important in TypeScript because it is the classic fix for the callback this problem. We will see that shortly.
Rule 4: new Binding
When you call a function with new, JavaScript creates a fresh object, sets this to that object inside the constructor, and returns it.
class Timer {
private seconds: number;
constructor(start: number) {
this.seconds = start; // this = new Timer instance
}
getTime(): number {
return this.seconds;
}
}
const t = new Timer(10);
console.log(t.getTime()); // 10
Inside a constructor (or a constructor function), this always refers to the object being created. This is the most predictable case.
Arrow Functions: Lexical this
Arrow functions do not have their own this. They inherit this from the surrounding lexical scope at the time they are defined. The four rules above do not apply to arrow functions.
class Counter {
private count = 0;
// Regular method: this depends on how it is called
increment(): void {
this.count++;
}
// Arrow method: this is always the Counter instance
incrementArrow = (): void => {
this.count++;
};
startRegular(): void {
setTimeout(this.increment, 1000); // Bug: this is undefined when called
}
startArrow(): void {
setTimeout(this.incrementArrow, 1000); // Works: this is still the Counter instance
}
startArrowInline(): void {
setTimeout(() => {
this.count++; // Works: arrow closes over this from startArrowInline
}, 1000);
}
}
The incrementArrow field is not on the prototype. It is created fresh for each instance. This is the trade-off: arrow methods avoid the this problem but use more memory because they are not shared via the prototype.
The Classic setTimeout Bug
This is one of the most common this bugs in JavaScript:
class Poller {
private data: string[] = [];
fetchData(): void {
// Simulating data fetch
this.data.push("item");
console.log(`Fetched ${this.data.length} items`);
}
startPolling(): void {
// Bug: when setTimeout calls this.fetchData, it loses the Poller context
setTimeout(this.fetchData, 1000);
}
}
const poller = new Poller();
poller.startPolling(); // TypeError: Cannot read properties of undefined (reading 'data')
setTimeout holds a reference to the function, but not to the object. When it eventually calls the function, this is undefined (strict mode).
There are three fixes:
class Poller {
private data: string[] = [];
fetchData(): void {
this.data.push("item");
console.log(`Fetched ${this.data.length} items`);
}
// Fix 1: Arrow function wrapper inline
startWithArrowWrapper(): void {
setTimeout(() => this.fetchData(), 1000);
}
// Fix 2: bind at the call site
startWithBind(): void {
setTimeout(this.fetchData.bind(this), 1000);
}
// Fix 3: Arrow class field (bind in the definition)
fetchDataBound = (): void => {
this.data.push("item");
console.log(`Fetched ${this.data.length} items`);
};
startWithBoundField(): void {
setTimeout(this.fetchDataBound, 1000);
}
}
Fix 1 is the most common in modern code. Fix 2 is explicit and useful when you cannot change the method definition. Fix 3 is useful when the method is passed as a callback in many places.
Class Method Pitfall in React and Callbacks
The same pattern appears everywhere callbacks are used: event emitters, array methods, Promise handlers, React event handlers (when using class components).
class EventProcessor {
private processed = 0;
// This will lose context when passed as a callback
handleEvent(event: { type: string }): void {
this.processed++;
console.log(`Handled ${event.type}, total: ${this.processed}`);
}
// This will always have the right context
handleEventBound = (event: { type: string }): void => {
this.processed++;
console.log(`Handled ${event.type}, total: ${this.processed}`);
};
}
const processor = new EventProcessor();
const events = [{ type: "click" }, { type: "keyup" }];
// Bug: handleEvent loses context inside forEach
events.forEach(processor.handleEvent); // this is undefined
// Fix 1: bind explicitly
events.forEach(processor.handleEvent.bind(processor));
// Fix 2: arrow wrapper
events.forEach((e) => processor.handleEvent(e));
// Fix 3: use the bound arrow field
events.forEach(processor.handleEventBound);
call vs apply vs bind: When to Use Each
| Method | Calls immediately | Arguments | Returns |
|---|---|---|---|
call | Yes | Spread: fn.call(ctx, a, b) | Return value of fn |
apply | Yes | Array: fn.apply(ctx, [a, b]) | Return value of fn |
bind | No | Spread (partial): fn.bind(ctx, a) | New bound function |
apply was useful before spread syntax for passing an unknown number of arguments. Today, fn.call(ctx, ...args) replaces most apply uses. bind is still the right choice when you need a reusable bound reference.
TypeScript and this Parameters
TypeScript lets you annotate this as a parameter in a function signature. This is a compile-time-only feature: the parameter is erased from the output. It lets TypeScript catch this misuse early.
interface Greeter {
name: string;
greet(): void;
}
function greetUser(this: Greeter): void {
console.log(`Hello, ${this.name}`);
}
const obj: Greeter = {
name: "Alice",
greet: greetUser,
};
obj.greet(); // OK
greetUser(); // Error: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Greeter'
This catches the "method removed from context" bug at compile time rather than at runtime.
Key insight: this is not about where the function is defined. It is about how the function is called. Arrow functions are the exception: they capture this from their definition context and never change it. When you pass a method as a callback, always check whether the call site will preserve the object context.