Arrow Functions in Depth
Mental model: An arrow has no identity of its own. No this, no arguments, no new. It borrows from where it was written.
Level: intermediate · about 11 minutes
const noArgs = () => 'hi'; // parens required when empty
const one = (n) => n + 1; // parens optional, keep them
const two = (a, b) => a + b;
const block = (n) => { const x = n * 2; return x; };
const object = (n) => ({ value: n }); // wrap the literal in parens
const rest = (...xs) => xs.length;
console.log(noArgs(), one(1), two(1, 2), block(3), object(4), rest(5, 6));Every variant of the syntax, shortest to longest.
- A concise body (no braces) returns its expression. A block body needs
return. - An object literal must be wrapped:
() => ({ ok: true }). - A single parameter can drop its parentheses, but keeping them survives adding a second parameter or a default.
- Arrows are always expressions, so they never hoist as callable names.
What an arrow does not have
| Missing | Consequence | Practical effect |
|---|---|---|
own this | it uses the this of the enclosing scope | perfect in callbacks, wrong as an object method |
own arguments | it sees the enclosing function argument list | use a rest parameter instead |
[[Construct]] | new fn() throws a TypeError | cannot be a constructor |
prototype | nothing to attach shared methods to | not usable for prototype based code |
super and new.target | inherited from the enclosing scope | fine inside class methods, not as one |
const arrow = () => {};
console.log(typeof arrow.prototype); // → 'undefined'
try { new arrow(); } catch (e) { console.log(e.constructor.name); } // → 'TypeError'
function outer() {
const inner = () => arguments.length; // reads OUTER arguments
return inner();
}
console.log(outer('a', 'b')); // → 2Three of those, demonstrated.
The object method trap
const badge = {
label: 'new',
renderMethod() { return `[${this.label}]`; }, // `this` is the object
renderArrow: () => `[${this?.label}]`, // `this` came from outside
};
console.log(badge.renderMethod()); // → '[new]'
console.log(badge.renderArrow()); // → '[undefined]'Same object, same property, two different results.
An arrow written inside an object literal captures the this of the code around the object, not the object. The object literal is not a scope, so there is nothing for the arrow to inherit except whatever this meant in the module or function containing it, which is rarely what you wanted.
Where an arrow is exactly right
Before arrows: save this by hand
const timer = {
label: 'build',
start() {
const self = this;
setTimeout(function () {
console.log(self.label);
}, 0);
},
};
With an arrow: nothing to save
const timer = {
label: 'build',
start() {
setTimeout(() => {
console.log(this.label);
}, 0);
},
};The method receives this from the call timer.start(), and the arrow inside it inherits that. This is the pattern arrows were added for, and it removes the var self = this line that used to be everywhere.
- Use an arrow for a callback Short, one expression, and it should see the surrounding
this. - Use a method for behaviour on an object Shorthand method syntax gets
thisfrom the call site, which is what a method needs. - Use a declaration when it must exist early or be a constructor Hoisting and
newboth require a non-arrow function.
const make = (n) => { value: n };
console.log(make(3));The braces are a block, not an object. Inside it, value: is read as a label on the expression statement n, which is evaluated and discarded, so the arrow returns undefined. Wrap the literal in parentheses: (n) => ({ value: n }).
Try it yourself
Method versus arrow
const project = {
name: 'lab',
tasks: ['write', 'test'],
describe() {
return `${this.name} has ${this.tasks.length} tasks`;
},
listLater() {
setTimeout(() => {
console.log(this.tasks.map((t) => `${this.name}:${t}`));
}, 0);
},
};
console.log(project.describe());
project.listLater();
Convert describe to an arrow and watch it break. Then convert the setTimeout callback to a regular function and watch that break instead.
Exercises
Fix the arrow method
The starter returns '[undefined]'. Fix makeBadge(label) so the returned object has a label property and a render method that reads this.label and returns it wrapped in square brackets. render must use this, so that borrowing it on another object with its own label uses that one.
Check yourself
- What does this log?
- undefined 10 — The arrow inherits
thisfrom the scope containing the object literal, which is not the object, sothis?.visundefined. The shorthand method receivesthisfrom the callobj.read(), so it sees10. - Which statement about arrows is false?
- They create their own
argumentsobject — Arrows do not createarguments. A reference inside one resolves to the enclosing function argument list, or throws at the top level. Use a rest parameter when you want the arguments of the arrow itself. - Why is an arrow the right choice for the callback inside a method?
- It inherits the method
this, so there is no need forvar self = this— The method already received the correctthisfrom its call site, and the arrow inherits it. That is precisely the problem arrows were designed to remove.
Common mistakes
- Writing object or class prototype methods as arrows, so
thisis not the instance. - Reaching for
argumentsinside an arrow instead of a rest parameter. - Using an arrow as a DOM event handler and expecting
thisto be the element.
Takeaways
- Arrows have no own
this,arguments,prototypeor construct behaviour. - They inherit
thislexically, which makes them ideal inside methods and wrong as methods. - A concise body returns its expression; an object literal needs wrapping parentheses.
- When you need hoisting,
new, or a realthis, use a declaration or a shorthand method.