Losing this
Troubleshoot methods that forget their object when they are detached, passed to callbacks, scheduled with setTimeout, or used as event handlers.
- 01Spot the broken call siteExplain why
obj.method()works butconst fn = obj.method; fn()does not. - 02Choose a repairUse a wrapper,
bind, a class-field arrow, or athisArgparameter intentionally. - 03Debug callbacks confidentlyPredict what
thisis in timers, array callbacks, and event listeners before you run them.
The disappearing receiver
In Methods & this, you saw that a method can use this to reach the object that called it. Lesson 15.1, The this keyword, names the rule: JavaScript decides this from the call site. Lesson 15.2, call, apply & bind, shows how to set it explicitly. Lesson 15.3, Arrow functions & lexical this, explains why arrows do not make their own this. This lesson turns those rules into troubleshooting muscle memory.
The practical problem is simple: a method works when you call user.greet(), then breaks when you pass it somewhere else. That is losing this. The function was not deleted. The object was not deleted. The relationship between the function call and the object disappeared.
A regular function does not carry its this around. Look at the expression that finally calls it. If there is no receiver before the dot, the method has probably lost the object it expected.
Losing this is like giving someone your phone’s speed-dial button without the phone. The button still exists, but the contact it pointed to is gone. Every fix in this lesson reattaches the button to the right phone before someone presses it.
- In real life: A phone stores contacts
- In JavaScript: An object stores data such as
nameorcount - In real life: A speed-dial button starts a call
- In JavaScript: A method starts behavior such as
greet() - In real life: Handing over only the button
- In JavaScript: Assigning
const fn = obj.method - In real life: Reattaching the button to the phone
- In JavaScript: Using a wrapper,
bind, a class-field arrow, orthisArg
Where the analogy stops: A real button may physically know which phone it belongs to. A JavaScript function value usually does not; regular this is selected by the next call.
We will cover detached methods, callbacks and setTimeout, bound class fields, and wrapper functions, in that order. Along the way you will run broken snippets, see the real error or wrong output, and choose the smallest repair.
Detached methods
STEP THROUGHThe smallest lost-this bug has two lines: copy a method, then call the copy. The copy is only a function value. It does not remember that it came from user.
Predict the call-site rule first. Then step through the recorded run to see whether the object before the dot survives.
script
"use strict"; name: "Ada", greet() { return "Hi, " + this.name; },};const fn = user.greet;console.log(fn());Read the broken version aloud: const fn = user.greet means “put the function currently stored at user.greet into fn.” It does not mean “put a future call to user.greet() into fn.” That difference is the whole bug.
In strict code, a plain call like fn() uses undefined as its default this. Then the method tries to read this.name, and a TypeError is the honest result. In older non-strict scripts, JavaScript may substitute a global object instead, which often turns the bug into a confusing undefined value rather than an immediate crash.
(user.greet)() still worksParentheses do not detach the method reference. The call still comes from the user.greet reference, so this is user. The bug starts when you store or pass the function value and call it later without the receiver.
Callbacks & setTimeout
INTERACTIVEA callback is a function you hand to another piece of code. That other code decides when and how to call it. If you hand over user.greet, you handed over a function, not the method call user.greet().
Four common callback shapes are worth recognizing on sight:
- Detached method:
const fn = user.greet; fn(). - Timer:
setTimeout(user.greet, 0)runs later without calling throughuser. - Event listener: browser event listeners usually receive the element as
this, not your counter object. - Array callback:
[1, 2].forEach(logger.log)callslogas a callback. Use its secondthisArgparameter when you wantthisto belogger.
"use strict";const user = { name: "Ada", greet() { return "Hi, " + this.name; },};const fn = user.greet;console.log(fn());Run a broken case, then toggle its fix.
This is intentionally broken. Look for the line that hands a method away as a plain function value.
Passing a callback is like letting an event host introduce your function on stage. The host may give it a different name badge. If your method expected the counter badge but the host gives it a button badge, the method reads and writes the wrong object.
- In real life: You lend a badge to a conference host
- In JavaScript: You pass a function to an API
- In real life: The host decides what badge you wear on stage
- In JavaScript: The API decides the callback's
this - In real life: A badge that says Button, not Counter
- In JavaScript: An event listener called with
thisas the element
Where the analogy stops: Some APIs do not use this at all, and arrows ignore the badge because they keep lexical this. Always check the API you are calling.
Bound class fields
STEP THROUGHClasses are a Stage 5 topic in this course, especially Class basics and Private fields & methods. For this lesson you only need one minimal pattern:
class Counter { count = 0; increment = () => { this.count++; };}increment = () => ... is a public field whose value is an arrow function. The arrow does not create a fresh this; it uses the instance being initialized. That makes the method already glued to the counter when you pass it to a callback.
Class syntax comes later in the course. For now, focus on the trade-off: class-field arrows are callback-safe, while prototype methods are shared.
script
count = 0; increment = () => { this.count++; return this.count; };} class PrototypeCounter { count = 0; increment() { this.count++; return this.count; }} const one = new BoundCounter();const two = new BoundCounter();const a = new PrototypeCounter();const b = new PrototypeCounter();console.log(one.increment === two.increment);console.log(a.increment === b.increment);The trace proves the trade-off. A prototype method is shared by all instances, so it is memory-efficient. A bound class field creates one function per instance, so it costs more function objects, but it is safe to hand to event listeners and timers. Use it when a method is designed to be used as a callback.
Wrapper functions
COMPARISONA wrapper is the most explicit fix: hand over a new function whose only job is to make the method call correctly. Instead of setTimeout(user.greet, 0), write setTimeout(() => user.greet(), 0). The timer calls the wrapper, and the wrapper calls user.greet() with the object before the dot.
| Fix | Shape | Best for | Watch out |
|---|---|---|---|
| Wrapper arrow | () => obj.method() | Keeps arguments flexible; best when you want to call the current object at run time. | Creates a tiny extra function and must be written at each callback site. |
bind | obj.method.bind(obj) | Creates a reusable function with this permanently attached. | Harder to undo; repeated binding creates new function identities. |
| Bound class field | increment = () => { this.count++; } | Great for UI callbacks because each instance method is already glued. | One function per instance instead of one shared prototype method. |
thisArg parameter | array.forEach(obj.method, obj) | Built into many array methods; no wrapper needed. | Only works for APIs that actually accept a thisArg. |
When an object has several callback methods, you may also bind them once. The helper below mutates the object, so use it deliberately. Notice the object uses inline method syntax; that keeps this lesson’s demos safe from production minifier rewrites that can accidentally change a local method call shape.
function bindMethods(obj, names) { for (const name of names) { obj[name] = obj[name].bind(obj); } return obj;} const cart = bindMethods({ items: 0, add() { this.items++; return this.items; },}, ["add"]); const addLater = cart.add;console.log(addLater());Practical debugging: look at the call site
SORTERWhen a method sees the wrong this, resist guessing. Walk through this checklist:
- Find the final call expression. What object, if any, is before the dot?
- Temporarily log
thisinside the method. In a browser, expand the value. - Use
console.trace()inside the method to see who called it. - Choose the smallest fix: wrapper,
bind, class-field arrow, orthisArg. - In TypeScript projects, consider
noImplicitThisso unclearthistypes become warnings.
Sort the call shapes below. Do not sort by where the function was defined. Sort by who performs the final call.
user.greet()const fn = user.greet; fn()() => user.greet()user.greet.bind(user)setTimeout(user.greet, 0)items.forEach(logger.log, logger)button.addEventListener("click", counter.increment)(user.greet)()
Put each expression where it belongs. Focus on the actual call site, not the method body.
Common misconceptions
“A method remembers the object where it was written.”
Regular functions do not. The same function body can run with different this values depending on the call site.
“Passing obj.method is the same as passing obj.method().”
The first passes a function value for later. The second calls immediately and passes the return value.
“bind changes the original method.”
bind returns a new function. Store that function if you want to reuse it.
“Arrow fields are always better than prototype methods.”
Arrow fields are callback-safe, but each instance gets its own function. Prototype methods are shared and excellent when you call them as methods.
“A timer is special, so it should remember the object.”
Timers schedule callbacks. They do not preserve the method receiver unless you wrap or bind the method yourself.
This lesson avoids the minifier hazard for this demos by using inline methods in object literals or module-level functions. That way production builds do not rewrite a local object method call into a detached function call and teach the wrong rule.
Practice: repair lost this
5 EXERCISESPredict whether the snippet prints a greeting, prints undefined, or throws.
"use strict";
const user = {
name: "Ada",
greet() {
return "Hi, " + this.name;
},
};
const fn = user.greet;
console.log(fn());It throws a TypeError because line 8 stores only the function and line 9 calls fn() without user before the dot.
Use bind so a detached speak callback still reads pet.name.
"use strict";
const pet = {
name: "Pixel",
speak() {
return this.name + " chirps";
},
};
const speak = pet.speak.bind(pet);
console.log(speak());"use strict";
const pet = {
name: "Pixel",
speak() {
return this.name + " chirps";
},
};
const speak = pet.speak.bind(pet);
console.log(speak());bind(pet) returns a new function whose this is permanently pet, so the detached call prints Pixel chirps.
Write the callback as a wrapper that calls the method on the object.
"use strict";
const queue = {
next: "Ada",
announce() {
return "Next: " + this.next;
},
};
const later = () => queue.announce();
console.log(later());"use strict";
const queue = {
next: "Ada",
announce() {
return "Next: " + this.next;
},
};
const later = () => queue.announce();
console.log(later());The wrapper is called later, then it makes a fresh queue.announce() method call. The output is Next: Ada.
Make increment safe to pass around by writing it as a class-field arrow.
class Counter {
count = 0;
increment = () => {
this.count++;
return this.count;
};
}
const counter = new Counter();
const click = counter.increment;
console.log(click());class Counter {
count = 0;
increment = () => {
this.count++;
return this.count;
};
}
const counter = new Counter();
const click = counter.increment;
console.log(click());The arrow field captures the instance this, so const click = counter.increment; click() prints 1.
Repair the array callback by passing the object as forEach’s thisArg.
"use strict";
const logger = {
prefix: "item ",
log(value) {
console.log(this.prefix + value);
},
};
[1, 2].forEach(logger.log, logger);"use strict";
const logger = {
prefix: "item ",
log(value) {
console.log(this.prefix + value);
},
};
[1, 2].forEach(logger.log, logger);Passing logger as the second argument makes this.prefix available for each callback call, so it prints item 1 and item 2.
Quiz: check your understanding
7 QUESTIONSEvery wrong answer explains the call-site mistake. Take your time and trace the final call.
Question 1 of 7Which call site sets
thistouser?Choose an answer to see the explanation.
Question 2 of 7What does this print or throw?
Read the code, then predict"use strict"; const user = { name: "Ada", greet() { console.log(this.name); }, }; const greet = user.greet; greet();Choose an answer to see the explanation.
Question 3 of 7Which fix keeps the method attached but still calls the current
userobject each time?Choose an answer to see the explanation.
Question 4 of 7What does this print?
Read the code, then predict"use strict"; const logger = { prefix: "#", log(value) { console.log(this.prefix + value); }, }; [1, 2].forEach(logger.log, logger);Choose an answer to see the explanation.
Question 5 of 7Why can bound class fields cost more memory than prototype methods?
Choose an answer to see the explanation.
Question 6 of 7What does this class-field callback print?
Read the code, then predictclass Counter { count = 0; increment = () => { this.count++; return this.count; }; } const counter = new Counter(); const inc = counter.increment; console.log(inc());Choose an answer to see the explanation.
Question 7 of 7Which debugging question should you ask first when
thisis wrong?Choose an answer to see the explanation.
Key takeaways
thisis chosen when a regular function is called, not when it is defined.- Detached methods lose their receiver:
const fn = obj.method; fn(). - Callbacks and timers receive function values; they do not preserve method receivers.
- Wrappers,
bind, bound class fields, andthisArgparameters all reattachthisin different ways. - Bound class fields are convenient for callbacks, but they create one function per instance.
Remember the one-liner.
Losing this means a method was called without the object it expected; fix it by making the call site explicit again.
Up next: Functions are objects, where you will see why functions can be stored, named, inspected, wrapped, and decorated.