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

MissingConsequencePractical effect
own thisit uses the this of the enclosing scopeperfect in callbacks, wrong as an object method
own argumentsit sees the enclosing function argument listuse a rest parameter instead
[[Construct]]new fn() throws a TypeErrorcannot be a constructor
prototypenothing to attach shared methods tonot usable for prototype based code
super and new.targetinherited from the enclosing scopefine 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'));                 // → 2

Three 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.

  1. Use an arrow for a callback Short, one expression, and it should see the surrounding this.
  2. Use a method for behaviour on an object Shorthand method syntax gets this from the call site, which is what a method needs.
  3. Use a declaration when it must exist early or be a constructor Hoisting and new both 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 this from the scope containing the object literal, which is not the object, so this?.v is undefined. The shorthand method receives this from the call obj.read(), so it sees 10.
Which statement about arrows is false?
They create their own arguments object — Arrows do not create arguments. 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 for var self = this — The method already received the correct this from 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 this is not the instance.
  • Reaching for arguments inside an arrow instead of a rest parameter.
  • Using an arrow as a DOM event handler and expecting this to be the element.

Takeaways

  • Arrows have no own this, arguments, prototype or construct behaviour.
  • They inherit this lexically, 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 real this, use a declaration or a shorthand method.