JavaScript Arrow Functions
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,setTimeoutcallbacks - ❌ 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/applyan arrow expectingthisto 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.