JavaScript Arrow Functions

6 minute read

Arrow functions (=>) are the compact, ES2015 form of function expression. They look like a smaller syntax — but their behavioural differences from regular functions are what really matter. No own this, no arguments, no prototype, can’t be new‘d. Knowing exactly when to reach for each form is one of the highest-leverage things you can learn in modern JS.

Syntax — All Variants

// Zero params — empty parens required
const greet = () => "Hi!";

// One param — parens optional, but most style guides require them
const square = x => x * x;
const square = (x) => x * x;        // ESLint preferred

// Multiple params — parens required
const add = (a, b) => a + b;

// Block body — braces, explicit return
const greetUser = (name) => {
  const greeting = `Hello, ${name}`;
  return greeting;
};

// Returning an object literal — wrap in parens
const wrap = name => ({ name });    // ✅ returns { name }
const wrong = name => { name };     // ❌ block body, returns undefined

// Default params, rest, destructuring — all work
const fn = ({ a = 1, b = 2 } = {}, ...rest) => [a, b, rest];
Param count Parens Body type Returns
0 () required expr / block expr / via return
1 optional (use them) expr / block same
2+ required expr / block same

Implicit Return

The single most common reason to use arrows: a one-expression function body returns the value automatically.

const double = x => x * 2;
const isAdmin = u => u.role === "admin";
const fullName = u => `${u.first} ${u.last}`;

[1, 2, 3].map(x => x * 2);              // [2, 4, 6]
users.filter(u => u.active);
items.reduce((sum, x) => sum + x.qty, 0);

Compared to the regular form:

[1, 2, 3].map(function (x) {
  return x * 2;
});

For tight callbacks the arrow form is unambiguously better.

The this Difference — The Real Reason They Exist

Regular functions get their own this, determined by how they’re called. Arrow functions don’t — they capture this from the surrounding (lexical) scope.

const counter = {
  count: 0,

  // Regular method — `this` is the object it was called on
  startA() {
    setInterval(function () {
      this.count++;            // ❌ `this` is window/undefined inside setInterval callback
      console.log(this.count);
    }, 1000);
  },

  // Arrow callback — captures `this` from startB
  startB() {
    setInterval(() => {
      this.count++;            // ✅ `this` is the counter object
      console.log(this.count);
    }, 1000);
  },
};
Regular function call site:
   setInterval(fn, 1000)
        │
        └── fn() with `this` = window/undefined
                       │
                       ▼
                 this.count++ — broken

Arrow inside startB:
        │ captures `this` from startB
        ▼
   this.count++ — points at counter

This is why arrows revolutionised callbacks. Pre-ES2015 you had to do var self = this; or .bind(this). With arrows, this just works.

When NOT to Use an Arrow

There are four situations where an arrow is wrong:

1. Object methods that need this

// ❌ Arrow captures outer `this` (probably window)
const user = {
  name: "Ada",
  greet: () => `Hi, I'm ${this.name}`,
};
user.greet();   // "Hi, I'm undefined"

// ✅ Method shorthand — `this` is the object
const user = {
  name: "Ada",
  greet() { return `Hi, I'm ${this.name}`; },
};
user.greet();   // "Hi, I'm Ada"

2. Class methods on a prototype

class Counter {
  count = 0;

  // ✅ Regular method — lives on prototype, `this` is the instance
  increment() {
    this.count++;
  }

  // ❌ Arrow as class field — works, but bound per-instance, costs memory & breaks `super`
  bad = () => { this.count++; };
}

(Bound arrows as class fields are sometimes useful — see the React event-handler pattern. But it’s a tradeoff, not a default.)

3. Constructors

Arrows can’t be called with new:

const Animal = (name) => { this.name = name; };
new Animal("Rex");   // ❌ TypeError: Animal is not a constructor

4. When you need arguments or function hoisting

const log = () => console.log(arguments);
log(1, 2);   // ❌ ReferenceError

// ✅ Use rest
const log = (...args) => console.log(args);

Other Behavioural Differences

Feature Regular function Arrow function
Own this ✅ Yes (depends on call site) ❌ Inherited from outer scope
Own arguments ✅ Yes ❌ Inherited
Own super depends inherits
Own new.target ✅ inherits
prototype property ✅ ❌ none
Constructable (new) ✅ ❌
Hoisted as declaration ✅ (the declaration form) ❌ (always expression)
Implicit return ❌ ✅ (single expression)
.bind, .call, .apply change this ✅ ❌ ignored for this

bind, call, apply — Don’t Affect Arrows

const obj = { x: 99 };

const regular = function () { return this.x; };
const arrow   = ()        => this?.x;     // captures lexical this

regular.call(obj);          // 99
arrow.call(obj);            // undefined  ← `this` is unchanged

regular.bind(obj)();        // 99
arrow.bind(obj)();          // undefined

That’s a feature — it’s why arrows work in callbacks. But it’s also why you must not use them as methods that rely on this.

Arrow as Default Callback

Arrows shine in higher-order array methods, event handlers (with caveats), promise chains, and timers:

// Array methods
users.map(u => u.name).filter(n => n.startsWith("A"));

// Promise chains
fetch("/api")
  .then(r => r.json())
  .then(data => data.items)
  .catch(err => console.error(err));

// Timer with shared `this`
class Timer {
  constructor() {
    this.seconds = 0;
    setInterval(() => this.seconds++, 1000);
  }
}

A Realistic Example — Currying

A curried function takes its arguments one at a time. Arrows make it concise:

const add = a => b => a + b;
add(2)(3);             // 5

const add5 = add(5);
add5(10);              // 15

// More general
const curry = fn => a => b => c => fn(a, b, c);
const sum3 = curry((a, b, c) => a + b + c);
sum3(1)(2)(3);         // 6

Same concept with regular functions:

function add(a) {
  return function (b) {
    return a + b;
  };
}

The arrow version is shorter, uses implicit return, and reads top-down. This is exactly the kind of tight functional code arrows were designed for.

Common Mistakes

// ❌ Arrow method losing `this`
const counter = {
  count: 0,
  inc: () => { this.count++; },
};
counter.inc();
console.log(counter.count);   // 0 — modified the outer this

// ❌ Block body without explicit return
const square = x => { x * x; };
square(5);   // undefined

// ❌ Returning object without parens
const make = name => { name };
make("Ada");   // undefined  — `name` is parsed as a label inside a block

// ✅
const make = name => ({ name });

// ❌ Single-arg arrow without parens, then adding a param later
const greet = name => `hi ${name}`;
const greet = name, greeting => `${greeting} ${name}`;   // ❌ SyntaxError
const greet = (name, greeting) => `${greeting} ${name}`; // ✅

Best Practices Checklist

  • ✅ Use arrows for callbacks and any function that doesn’t need its own this
  • ✅ Use method shorthand (greet() { … }) for object/class methods
  • ✅ Use parens around even single parameters — consistent with linters
  • ✅ Wrap returned object literals in parens: x => ({ key: value })
  • ✅ Reach for arrows in map/filter/reduce, promise chains, setTimeout callbacks
  • ❌ Don’t use arrows as object methods that read this
  • ❌ Don’t use arrows as constructors
  • ❌ Don’t use arrows when you need arguments (use ...rest)
  • ❌ Don’t bind/call/apply an arrow expecting this to change

What’s Next?

You can write functions in both styles. Next up — the rules around scope: where variables exist, which scope wins on a name clash, and how the scope chain decides what someVariable actually means at any given line.

← Previous: JS Functions Next: JS Scope →

Updated: