Arrow functions & lexical this
Learn why arrow functions inherit this, how that helps callbacks and timers, and why arrows are the wrong shape for object methods and many event handlers.
- 01Explain lexical thisDescribe why an arrow uses the this from where it was written.
- 02Choose callback shapesUse arrows in timers and array callbacks when they need the surrounding method’s this.
- 03Avoid method trapsPrefer method shorthand for object methods and event.currentTarget for event code.
The sticky-note rule
You already met arrow function syntax in the Arrow functions lesson. This lesson goes deeper on the one arrow feature that changes how real programs feel: arrows do not create their own this. A regular function asks, “who is calling me right now?” every time it runs. An arrow never asks. It uses the this from the place where it was written.
That behavior is called lexical this. The word lexical means “based on where the code is written,” the same idea you saw in Scope and lexical environments and Closures. Variables are found by looking outward through the written scopes. An arrow’s this is found the same way.
An arrow function captures this from its surrounding code; call, apply, and bind cannot change that captured value.
Imagine writing a sticky note inside a room: “use this room.” If you hand the note to someone in the hallway, the note does not change itself to say “hallway.” Arrow functions behave like that note.
- In real life: A room label on the door
- In JavaScript: The surrounding function’s
this - In real life: A sticky note saying “same room as where I was written”
- In JavaScript: An arrow function’s lexical
this - In real life: Moving the note to another desk
- In JavaScript: Passing the arrow to a callback, timer, or
call - In real life: The note still names the original room
- In JavaScript: The arrow still uses the captured
this
Where the analogy stops: A sticky note is copied text; an arrow captures a live binding to a this value chosen by JavaScript. The analogy explains why moving the arrow later does not relabel it.
That makes arrows wonderful in callbacks and timers, where a regular function is often called later with no useful receiver. It also makes arrows a poor fit for object methods and for event handlers that rely on the browser’s this. By the end, you will know which shape to choose before you type the function.
Lexical this: arrows do not ask the call site
STEP THROUGHThe this keyword lesson focuses on call-site rules: default, implicit, explicit, and new binding. Regular functions participate in those rules. If you run obj.fn(), the receiver before the dot supplies this. If you run fn.call(other), call supplies it explicitly.
Arrows are different. Their this is chosen when the arrow is created, by the surrounding code. The next experiment creates an arrow inside an object method. Then it tries to use call to relabel the arrow. Predict the output before you step.
Predict whether line 8 prints studio or garage, then step through the call.
script
"use strict"; name: "studio", makeReader() { return () => this.name; }}; const reader = room.makeReader();console.log(reader.call({ name: "garage" }));The surprising line is the last one. reader.call looks as if it should set this to { name: "garage" }. It would work on a regular function. But reader is an arrow that was created while room.makeReader() was running, and that method’s this was room. The arrow keeps reading the room.
Regular functions are like visitors who check the sign at the door every time they enter. Arrows are like visitors with a badge already printed: moving them to a new line does not reprint the badge.
- In real life: A visitor asks security, “which company am I visiting?”
- In JavaScript: A regular function asks the call site for
this - In real life: A badge printed before the visit
- In JavaScript: An arrow’s
thisdecided at creation time - In real life: Putting the visitor in a different line
- In JavaScript: Using
call,apply, orbindlater
Where the analogy stops: Real people can ask again. Arrow functions cannot; there is no own this binding to replace.
Calling bind on an arrow is allowed, but it cannot bind this. It can still pre-fill arguments, so the method is not useless; it just cannot relabel the arrow’s this.
Arrows in callbacks & timers
INTERACTIVECallback code often runs later, or inside another helper. A timer API does not call your callback as timer.callback(). Array methods do not call your callback as object.callback(). They usually call it as a plain function. In strict code, a plain regular-function call has this set to undefined.
The next demo uses a synchronous helper named runLater so you can see the rule without waiting for a real clock. It mimics the important timer behavior: a callback is stored, then invoked as callback(). Switch the callback style and follow the frames.
Switch the callback style. Regular functions ask who called them; arrows keep the method’s this.
script
"use strict";function runLater(callback) { callback();} label: "Focus", seconds: 0, start() { runLater(function () { this.seconds += 1; console.log(this.label + ": " + this.seconds); }); }}; try { timer.start();} catch (error) { console.log(error.name);}With the regular callback, the method call timer.start() correctly gives start a timer-shaped this. But the callback is a separate function. When runLater calls it with no receiver, the callback asks “who called me?” and the answer is “nobody useful.” The update fails.
With the arrow callback, the callback never asks runLater for a new this. It uses the this that start already had. This is the common reason you see arrows inside setTimeout, setInterval, map, forEach, and promise callbacks.
"use strict";const tagger = { prefix: "JS-", ids: [1, 2, 3], withArrow() { return this.ids.map((id) => this.prefix + id); }, withSelf() { const self = this; return this.ids.map(function (id) { return self.prefix + id; }); }, withBind() { return this.ids.map(function (id) { return this.prefix + id; }.bind(this)); }}; console.log(tagger.withArrow().join("|"));console.log(tagger.withSelf().join("|"));console.log(tagger.withBind().join("|"));Older code often wrote const self = this before a regular callback, or used .bind(this) after it. Those patterns are still useful to recognize. In modern code, the arrow version is usually shorter and clearer: “use the same this as the method I’m inside.”
Arrows as methods: the trap
COMPAREThe arrow rule helps when an inner callback needs the outer method’s this. It hurts when the arrow itself is supposed to be the method. Object methods normally need the object on the left of the dot to become this. An arrow method ignores that dot.
"use strict";function makeCounter() { return { count: 0, inc: () => this.count++ };} const counter = makeCounter.call(undefined);try { counter.inc();} catch (error) { console.log(error.name);} const safeCounter = { count: 0, inc() { this.count++; return this.count; }};console.log(safeCounter.inc());In the strict factory shown above, the outer this captured by the arrow is undefined. So inc: () => this.count++ does not mean “increment counter.count.” It means “increment count on the outer this,” and there is no outer object to update.
Notice that the working object uses inline method shorthand: inc() { ... }. For this examples, avoid building a local object from a separately declared function, because production minifiers can rewrite the call in a way that hides the receiver. Inline methods keep the demonstration honest.
| Situation | Regular function | Arrow function |
|---|---|---|
Method call obj.fn() | this is usually obj | Ignores obj; uses outer this |
Plain call fn() in strict code | this is undefined | Still uses outer this |
| Callback inside a method | Often loses the method’s this unless bound | Keeps the method’s this |
| Event listener | Browser sets this to event.currentTarget | Uses outer this; prefer event.currentTarget |
call / apply / bind | Can set or pre-bind this | Cannot change this; can still pass arguments |
new | Can construct if written as a constructor | Throws: arrows are not constructors |
Class fields are the one “method-looking” exception worth knowing: handleClick = () => { ... } creates an arrow per instance, so it captures that instance’s this. That is a bound-method pattern you will see again in Losing this and in the Classes stage. For ordinary object literals, choose method shorthand.
this in event handlers
REAL BUTTONThe DOM and events come later in the course, but this in event handlers is too common to ignore. Browsers call a regular addEventListener listener with this set to the element whose listener is running. The event object gives you the same element as event.currentTarget.
Arrow listeners do not receive that browser-provided this. They keep the outer this instead. In React components and ES modules, that outer value is not the button. Click this real button and compare both listener styles.
button.addEventListener("click", function (event) { console.log(this === event.currentTarget);}); button.addEventListener("click", (event) => { console.log(this === event.currentTarget); console.log(event.currentTarget.textContent);});- WaitingClick the button to log both listeners.
Click the real button or dispatch a click. Two listeners run: a regular function and an arrow. Compare their this values with event.currentTarget.
The practical rule is simple: use event.currentTarget instead of this when in doubt. It works with a regular listener, an arrow listener, and code that gets refactored later.
button.addEventListener("click", (event) => { event.currentTarget.disabled = true; event.currentTarget.textContent = "Saved";});Where you will use this
SORTThe choice is not “arrows are modern, regular functions are old.” The choice is “does this function need its own caller-chosen this, or should it keep the surrounding one?” That question appears in UI handlers, data transformations, timers, class components, and tests.
- An object method that reads
this.count - A
forEachcallback inside a method that readsthis.prefix - An event listener that intentionally uses
thisas the clicked element - A
mapcallback that only uses its parameter - A class field handler that should stay bound when passed around
- A function that will be called with
newIt belongs in Regular function. Arrow functions cannot be constructors. They do not have
[[Construct]], sonewthrows.
Sort each situation by the function shape that makes this safest.
A useful habit is to write the receiver in your sentence. If the sentence is “this object should do the work,” use method shorthand or a regular function. If the sentence is “this inner callback should use the same object as the surrounding method,” use an arrow. If no this is involved, choose the style that is easiest to read.
Common misconceptions
- “Arrows bind this to themselves.” They do the opposite: they have no own
thisbinding. - “bind fixes every function.”
bindfixes regular functions. It cannot change an arrow’s capturedthis. - “Arrows are always better callbacks.” They are great when you want lexical
this, but a regular event listener is useful when you intentionally want the browser’sthis. - “An arrow method reads the object before the dot.” The dot does not matter to an arrow. Use method shorthand for object behavior.
- “event.target and event.currentTarget are the same.” They match for simple buttons, but child elements can be the target. The listener’s element is
currentTarget.
Practice exercises
5 EXERCISESRead the code. What exact error name is printed?
"use strict";
function makeCounter() {
return {
count: 0,
inc: () => this.count++
};
}
const counter = makeCounter.call(undefined);
try {
counter.inc();
} catch (error) {
console.log(error.name);
}TypeErrorThe arrow ignores counter.inc() as a receiver. It tries to read this.count from the outer this, which is undefined, so the caught error name is TypeError.
Rewrite a broken regular timer callback as an arrow. What does the fixed code print?
"use strict";
const timer = {
label: "Tea",
start() {
setTimeout(() => {
console.log(this.label);
}, 0);
}
};
timer.start();"use strict";
const timer = {
label: "Tea",
start() {
setTimeout(() => {
console.log(this.label);
}, 0);
}
};
timer.start();The arrow is written inside start, so it keeps start’s this. When the timer callback runs, it still reads timer.label and prints Tea.
Replace the inner function and self alias with an arrow callback.
"use strict";
const list = {
prefix: "#",
values: [1, 2, 3],
labels() {
const self = this;
return this.values.map(function (value) {
return self.prefix + value;
});
}
};
console.log(list.labels().join(","));
// Convert labels() to the arrow version."use strict";
const list = {
prefix: "#",
values: [1, 2, 3],
labels() {
return this.values.map((value) => this.prefix + value);
}
};
console.log(list.labels().join(","));The arrow callback closes over the same this that labels() received. It produces the same #1,#2,#3 output without the self alias.
A cart object has an add method that changes this.total. Which style is safer?
const cart = {
total: 0,
add(price) {
this.total += price;
}
};Use method shorthand. cart.add(5) should set this to cart; an arrow would ignore that receiver.
Make this arrow listener disable the clicked button without relying on this.
button.addEventListener("click", () => {
this.disabled = true;
});button.addEventListener("click", (event) => {
event.currentTarget.disabled = true;
});An arrow listener should use event.currentTarget for the element whose listener is running. That rule still works if you later change the listener shape.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does lexical
thismean for an arrow function?Choose an answer to see the explanation.
Question 2 of 7What does the call-relabeled arrow print?
Read the code, then predict"use strict"; const tool = { name: "Brush", show() { const read = () => this.name; console.log(read.call({ name: "Drill" })); } }; tool.show();Choose an answer to see the explanation.
Question 3 of 7Which callback is usually best inside a method when the callback needs the method’s
this?Choose an answer to see the explanation.
Question 4 of 7What error name does the arrow method print?
Read the code, then predict"use strict"; function makeCounter() { return { count: 0, inc: () => this.count++ }; } const counter = makeCounter.call(undefined); try { counter.inc(); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 5 of 7In a browser
addEventListenerregular function listener, what isthisnormally set to?Choose an answer to see the explanation.
Question 6 of 7What happens if you use
newwith an arrow function?Choose an answer to see the explanation.
Question 7 of 7When in doubt inside an event handler, which value should you read instead of relying on
this?Choose an answer to see the explanation.
Key takeaways
- Regular functions get
thisfrom how they are called. - Arrow functions get
thisfrom where they are written. - Use arrows for callbacks that need the surrounding method’s
this. - Use method shorthand for object methods that read or write
this. - In event handlers,
event.currentTargetis clearer than relying onthis.
Arrow functions are functions with lexical this: they keep the surrounding this instead of receiving a new one from the call site.
Up next: Losing this.