JavaScript this binding is decided by the call site — the exact way a function is invoked — and not by where the function was written or what it is a method of. That single rule explains every confusing this bug you will meet: the method works fine until somebody passes it to a callback, and then it quietly starts pointing at something else.
This page walks through the rules that decide this, shows the three ways to take control of it, and ends with the practical order of preference for fixing a broken method.
Why does this change when a method is passed around?
Here is a method that works when you call it normally, and breaks the moment you pull it off the object.
class Stopwatch {
constructor(label) {
this.label = label;
this.elapsed = 0;
}
report() {
return this.label + ": " + this.elapsed + "s";
}
}
const watch = new Stopwatch("focus");
console.log(watch.report());
const detached = watch.report;
try {
detached();
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
Calling watch.report() prints focus: 0s. Assigning the method to a variable and calling it there throws a TypeError reading Cannot read properties of undefined (reading 'label'). Nothing about the function changed — only the call did. The first call had a receiver (watch), the second had none, and with no receiver this is undefined inside a class body, because class bodies are always strict.
This is worth separating from a related mistake. If report had been an ordinary function in an object literal in loose code, calling it detached would not throw — this would silently become the global object, and the bug would surface much later as a corrupted piece of unrelated data. Strict mode turns a silent corruption into a loud error, which is one of the better arguments for it.
What are the rules that decide this?
There are four of them, and they are tried in a fixed order. The first one that applies wins.
| How the function is called | What this is |
|---|---|
fn() with no receiver | undefined in strict mode, the global object in loose mode |
obj.fn() | obj |
fn.call(x) or fn.apply(x) | x |
new Fn() | The fresh object the constructor created |
| An arrow function | Whatever this was where the arrow was created |
The first four are worth checking in a console, because seeing them behave differently is the fastest way to make the table stick.
"use strict";
function whoAmI() {
return this;
}
console.log(whoAmI());
const watch = { label: "focus", whoAmI };
console.log(watch.whoAmI() === watch);
console.log(whoAmI.call(watch) === watch);
console.log(whoAmI.apply(watch) === watch);
The first line prints undefined, because a plain function called with no receiver gets no this at all in strict mode. The next three lines all print true: the method form, call and apply all end up with watch as this.
The arrow function row is the exception and it is the one that saves the day most often. An arrow function has no this of its own at all — it closes over the this of the scope it was created in. That is why this inside an arrow function can never be undefined and can never be rebound with call, apply or bind.
What do call, apply and bind actually do?
All three let you supply a receiver explicitly. call takes the receiver followed by the arguments, apply takes the receiver followed by an array of arguments, and bind takes only the receiver and returns a new function with it already attached.
class Stopwatch {
constructor(label) {
this.label = label;
this.elapsed = 0;
}
tick(seconds) {
this.elapsed += seconds;
return this.label + " at " + this.elapsed + "s";
}
}
const watch = new Stopwatch("focus");
const tick = watch.tick;
console.log(tick.call(watch, 5));
console.log(tick.apply(watch, [10]));
const boundTick = tick.bind(watch);
console.log(boundTick(15));
console.log(boundTick.name);
console.log(boundTick === tick);
The three calls print focus at 5s, focus at 15s and focus at 30s, because they all add to the same elapsed time on the same watch. bind returns a brand new function — the last line prints false — and the new function carries the name bound tick, which is a small but useful clue when you are reading a stack trace.
call and apply change the receiver for one call and then let it go. bind changes it permanently. When you want a method to survive being handed to a callback, bind is the one you want.
Why do arrow functions fix the problem so neatly?
Because an arrow function copies the this of its surrounding scope at the moment it is created. Declare it inside a method and it carries that method's this with it forever, no matter where it is later called.
class Stopwatch {
label = "stopwatch";
elapsed = 0;
constructor(label) {
this.label = label;
}
tick = () => {
this.elapsed += 1;
return this.label + " at " + this.elapsed + "s";
};
}
const watch = new Stopwatch("focus");
const detached = watch.tick;
console.log(detached());
console.log(detached());
console.log(watch.elapsed);
Detaching the method no longer breaks anything. The two calls print focus at 1s and focus at 2s because both arrows carry the watch's this, and watch.elapsed ends up at 2 — the state really did change on the original object.
The cost is that the arrow is a per-instance property rather than something shared on the prototype. Every stopwatch gets its own copy of the function, and because it is an own property it also shows up when you enumerate the object's keys. For a handful of instances that is irrelevant; for ten thousand of them it is ten thousand functions instead of one.
Why does a method lose this inside a callback?
Because every callback API calls your function without a receiver. map, forEach, setTimeout and event listeners all invoke their callback with the arguments they want and nothing else, so this inside your method falls back to the no-receiver rule.
class Playlist {
constructor(name) {
this.name = name;
}
format(track) {
return this.name + ": " + track;
}
}
const playlist = new Playlist("focus");
const tracks = ["alpha", "beta"];
try {
console.log(tracks.map(playlist.format));
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
console.log(tracks.map((track) => playlist.format(track)));
console.log(tracks.map(playlist.format.bind(playlist)));
Passing the method straight into map throws a TypeError reading Cannot read properties of undefined (reading 'name'), because map calls it with no receiver. Wrapping it in an arrow function works and prints an array holding the two strings focus: alpha and focus: beta. Binding it works too and prints the same array.
One more trap hides here. Array callbacks receive three arguments: the element, the index and the whole array. If your method takes a second parameter, playlist.format used as a callback will receive the index where you expected your own argument. That is why the arrow wrapper in the example above is usually the safer fix — it forwards exactly the arguments you choose.
What about timers and event handlers?
They are the same problem wearing different clothes. A timer or a listener stores your function and calls it later, with no receiver and no interest in your object.
class Poller {
constructor(url) {
this.url = url;
this.attempts = 0;
}
poll() {
this.attempts += 1;
console.log("attempt " + this.attempts + " for " + this.url);
}
}
const poller = new Poller("/api/status");
setTimeout(poller.poll.bind(poller), 0);
setTimeout(() => poller.poll(), 0);
Both timers fire and both print attempt 1 for /api/status and attempt 2 for /api/status, because both managed to keep this pointing at the poller. Handing poller.poll to setTimeout directly would have failed with the same TypeError you saw with map.
For DOM event listeners the arrow wrapper is the usual pattern, because the listener receives an event object that you want to pass on to a real method.
In what order should you reach for a fix?
- First, check whether the method needs
thisat all. A pure function that takes everything as arguments has no binding problem to solve. - Then use an arrow class field when the method is a callback you hand to other code. It is the smallest change and it cannot be unbound by accident.
- Use
bindwhen you need to keep the method as a shared prototype method, or when you are attaching a prototype method to an event listener. - Use
callandapplyonly when you genuinely want to swap the receiver for a single call — they are a tool for borrowing behaviour, not a repair kit. - Never reach for
thisas an argument, for examplefunction (self) { self.x }. It works, but arrow functions andbindsay the same thing more clearly.
Try it yourself: build a class Form with a constructor that stores this.fields = [], a method addField(name) that pushes onto that array, and a method render() that returns the field names joined with commas. Now write const names = ["email", "password"]; and try names.forEach(form.addField); — it will throw, because forEach calls your method without a receiver. Fix it three different ways: wrap the call in an arrow function, bind the method to the form, and convert the method into an arrow class field. Then check form.fields.length is 2 after each version, and Object.keys(form) to see which fixes added an own property. Finally, call const r = form.render; and observe that it throws for the same reason map did — a method that reads this always needs a receiver.