Arrow functions look like a shorthand for function expressions, and for simple callbacks they are exactly that. But they have six behavioral differences that matter โ and using an arrow function where a regular function is required is one of the most common sources of this-related bugs.
The Six Differences
### 1. this Binding โ The Big One
Regular functions create their own this at call time, determined by how they are called. Arrow functions have no this of their own โ they inherit this from the lexical scope where they were defined.
const user = {
name: 'Alice',
// Regular function: 'this' is the object at call time greetRegular: function() { return Hello, I'm ${this.name} },
// Arrow function: 'this' inherited from where the arrow was written // At definition time, the object literal isn't a scope โ the outer scope is greetArrow: () => { return Hello, I'm ${this.name} }, }
user.greetRegular() // "Hello, I'm Alice" โ this = user โ user.greetArrow() // "Hello, I'm undefined" โ this = outer scope โ
The arrow function was written inside an object literal, but object literals don't create a new scope. So this goes to the surrounding scope โ in a browser, that's window (or undefined in strict mode). Neither has a name property.
Where arrow functions shine โ preserving this in callbacks:
class Timer {
constructor() {
this.seconds = 0
}
start() { // Regular function: 'this' is undefined in strict mode (or window in sloppy) // setInterval(function() { this.seconds++ }, 1000) // broken
// Arrow function: inherits 'this' from start() โ the Timer instance setInterval(() => { this.seconds++ console.log(this.seconds) }, 1000) } }
const t = new Timer() t.start() // 1, 2, 3... โ works correctly because of arrow's lexical this
Before arrow functions, the standard fix was const self = this or .bind(this). Arrow functions made this unnecessary.
### 2. arguments Object
Regular functions have an implicit arguments object โ an array-like containing all arguments passed to the function. Arrow functions do not have arguments โ if you reference arguments inside an arrow function, you get the arguments of the nearest enclosing regular function.
function regular() {
console.log(arguments) // Arguments [1, 2, 3]
}
regular(1, 2, 3)
const arrow = () => { console.log(arguments) // ReferenceError (or outer function's arguments) } arrow(1, 2, 3)
In modern code, use rest parameters instead โ they work in both, are a real Array (with .map(), .filter()), and are more explicit:
const arrow = (...args) => {
console.log(args) // [1, 2, 3] โ a real Array
}
arrow(1, 2, 3)
### 3. Constructor Usage
Regular functions can be used as constructors with new. Arrow functions cannot โ they throw a TypeError.
function Person(name) {
this.name = name
}
const alice = new Person('Alice') // { name: 'Alice' } โ
const Animal = (name) => { this.name = name } const cat = new Animal('Cat') // TypeError: Animal is not a constructor โ
Arrow functions don't have a prototype property and don't have their own this, so they can't function as constructors. If you need a constructor, use a regular function or a class.
### 4. prototype Property
Regular functions automatically get a prototype property (used by the prototype chain when called with new). Arrow functions do not.
function Fn() {}
Fn.prototype // { constructor: Fn }
const ArrowFn = () => {} ArrowFn.prototype // undefined
This is consistent with arrow functions not being constructors โ without prototype, there's no way to set up the prototype chain for new-created instances.
### 5. Generator Functions
Regular functions can be generator functions using function*. Arrow functions cannot be generators.
function* generator() {
yield 1
yield 2
yield 3
}
const arrowGen = *() => { ... } // SyntaxError โ not supported
If you need a generator, use function*.
### 6. new.target
Regular functions can check new.target to determine if they were called with new. Arrow functions don't have new.target โ they inherit it from the enclosing scope.
function Foo() {
if (!new.target) {
return new Foo() // Called without new โ redirect
}
this.x = 1
}
new Foo() // { x: 1 } Foo() // also { x: 1 } โ redirected
Decision Matrix
| Use case | Regular function | Arrow function | |---|---|---| | Object method | โ | โ (loses object's this) | | Class method | โ | โ (as class field โ binds to instance) | | Constructor | โ | โ | | Prototype method | โ | โ | | Array callback (map, filter) | โ | โ | | setTimeout / setInterval callback | โ (with bind) | โ (lexical this) | | Event handler (needs this as element) | โ | โ | | Generator | โ | โ | | Needs arguments object | โ | โ |
Where Arrow Functions Are Always Better
Array callbacks โ you almost never need this or arguments here, and shorter syntax is cleaner:
const result = items
.filter(item => item.active)
.map(item => item.name)
.reduce((acc, name) => ({ ...acc, [name]: true }), {})
Callbacks that need to preserve outer this:
class Component {
handleClick() {
fetch('/api/data')
.then(res => res.json())
.then(data => {
this.setState(data) // arrow inherits 'this' from handleClick โ the component
})
}
}
Single-expression functions:
const double = n => n * 2
const isEven = n => n % 2 === 0
const getKey = obj => obj.id
Class Field Arrow Functions โ A Special Case
Arrow functions as class fields create instance-specific methods โ each instance gets its own copy of the function, with this permanently bound to that instance.
class Button {
// Arrow class field โ creates a new function per instance
// 'this' is always the instance, even when used as a callback
handleClick = () => {
console.log(this.label)
}
constructor(label) { this.label = label } }
const btn = new Button('Submit') const { handleClick } = btn // extract it handleClick() // "Submit" โ 'this' is permanently bound to btn
This is common in React class components and event handler patterns. The cost: each instance gets its own copy of the function (vs prototype methods which are shared). For most components this is negligible.
The Rule
Use arrow functions for:
- Callbacks and short expressions
- Anywhere you want to inherit
thisfrom the outer scope
Use regular functions for:
- Object methods that need their own
this
- Constructors and prototype methods
- Generator functions
- Functions that need the
argumentsobject
When in doubt: if you're writing a method that belongs to an object or class, use a regular function or method syntax. If you're writing a callback, use an arrow function.