When to Avoid Using Arrow Functions in JavaScript
Learn when arrow functions are the wrong choice in JavaScript. Covers object methods, prototype methods, constructors, event handlers needing this, arguments object usage, and generator functions where regular functions are required.
Arrow functions are excellent for callbacks, inline functions, and short utilities. But they are not a universal replacement for regular functions. Their lexical this binding, lack of arguments object, and inability to serve as constructors make them the wrong choice in several common scenarios. Using an arrow function where a regular function is needed creates bugs that can be difficult to track down because the code looks correct but behaves unexpectedly.
This tutorial covers every situation where arrow functions cause problems and shows the correct alternative for each case.
Problem 1: Object Methods
Arrow functions inherit this from their enclosing scope. When used as object methods, this does not point to the object:
// Bug: arrow function as object method
const user = {
name: "Alice",
greet: () => {
return `Hello, I'm ${this.name}`; // 'this' is NOT the user object
},
};
console.log(user.greet()); // "Hello, I'm undefined"The arrow function's this comes from the surrounding scope (likely the global object or undefined in modules), not from the user object.
Fix: Use Method Shorthand or Regular Function
// Correct: method shorthand
const user = {
name: "Alice",
greet() {
return `Hello, I'm ${this.name}`;
},
};
console.log(user.greet()); // "Hello, I'm Alice"// Also correct: regular function expression
const user = {
name: "Alice",
greet: function () {
return `Hello, I'm ${this.name}`;
},
};
console.log(user.greet()); // "Hello, I'm Alice"| Approach | this value | Recommended |
|---|---|---|
| Arrow function | Enclosing scope | No |
| Method shorthand | The object | Yes |
| Regular function | The object | Yes |
Object Methods Must Be Regular Functions
Any method that needs to access the object via this must be a regular function or method shorthand. Arrow functions will always look up this from the outer scope, ignoring the object entirely.
Problem 2: Prototype Methods
The same this issue affects prototype methods:
// Bug: arrow function on prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = () => {
return `Hi, I'm ${this.name}`; // 'this' is NOT the instance
};
const alice = new Person("Alice");
console.log(alice.greet()); // "Hi, I'm undefined"Fix: Use Regular Function
Person.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
const alice = new Person("Alice");
console.log(alice.greet()); // "Hi, I'm Alice"Problem 3: Constructor Functions
Arrow functions cannot be used with new. They have no prototype property and no internal [[Construct]] method:
// Bug: arrow function as constructor
const Car = (make, model) => {
this.make = make;
this.model = model;
};
// TypeError: Car is not a constructor
// const myCar = new Car("Toyota", "Camry");Fix: Use Regular Function or Class
// Regular constructor function
function Car(make, model) {
this.make = make;
this.model = model;
}
const myCar = new Car("Toyota", "Camry");
console.log(myCar.make); // "Toyota"// ES6 class (preferred in modern code)
class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
}
const myCar = new Car("Toyota", "Camry");
console.log(myCar.make); // "Toyota"Problem 4: Event Handlers That Need this
In DOM event handlers, this refers to the element that triggered the event. Arrow functions break this behavior:
// Bug: arrow loses the element reference
const button = document.getElementById("submit");
button.addEventListener("click", () => {
console.log(this); // Window (or undefined in modules)
this.classList.add("active"); // Error or wrong element
});Fix: Use Regular Function
button.addEventListener("click", function () {
console.log(this); // <button id="submit">
this.classList.add("active"); // correct element
});When Arrow Functions ARE Fine in Event Handlers
If you access the element through the event parameter instead of this, arrows work perfectly:
button.addEventListener("click", (event) => {
console.log(event.target); // <button id="submit">
event.target.classList.add("active"); // correct, using event.target
});| Access method | Arrow function | Regular function |
|---|---|---|
this | Wrong (enclosing scope) | Correct (element) |
event.target | Correct | Correct |
event.currentTarget | Correct | Correct |
Problem 5: The arguments Object
Arrow functions do not have their own arguments object:
// Bug: arguments is not available
const logArgs = () => {
console.log(arguments); // ReferenceError (or inherits from outer function)
};
// logArgs(1, 2, 3); // ErrorIf an arrow function is inside a regular function, it inherits that function's arguments, which creates confusing behavior:
function outer() {
const inner = () => {
console.log(arguments); // outer's arguments, not inner's
};
inner("ignored");
}
outer("hello", "world");
// Arguments ["hello", "world"] -- from outer, not innerFix: Use Rest Parameters or Regular Function
// Rest parameters (preferred)
const logArgs = (...args) => {
console.log(args); // [1, 2, 3]
};
logArgs(1, 2, 3);// Regular function (when arguments object is specifically needed)
const logArgs = function () {
console.log(arguments); // Arguments [1, 2, 3]
};
logArgs(1, 2, 3);Problem 6: Dynamic this with call, apply, bind
Arrow functions ignore call, apply, and bind because their this is permanently fixed:
const greet = () => {
return `Hello, ${this.name}`;
};
const user = { name: "Alice" };
// .call, .apply, .bind have no effect on arrow functions
console.log(greet.call(user)); // "Hello, undefined"
console.log(greet.apply(user)); // "Hello, undefined"
const bound = greet.bind(user);
console.log(bound()); // "Hello, undefined"Fix: Use Regular Function
const greet = function () {
return `Hello, ${this.name}`;
};
const user = { name: "Alice" };
console.log(greet.call(user)); // "Hello, Alice"
console.log(greet.apply(user)); // "Hello, Alice"
const bound = greet.bind(user);
console.log(bound()); // "Hello, Alice"Problem 7: Generator Functions
Arrow functions cannot be generators. There is no =>* syntax:
// SyntaxError: not possible
// const gen = *() => { yield 1; };Fix: Use Regular Generator Function
function* counter(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for (const num of counter(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}Complete Decision Table
| Situation | Use arrow | Use regular | Why |
|---|---|---|---|
| Array callback (map, filter, reduce) | Yes | Okay | Arrows are concise; this not needed |
| Promise .then() callback | Yes | Okay | Lexical this is usually desired |
| setTimeout/setInterval callback | Yes | With caution | Arrows preserve outer this |
| Object method | No | Yes | Method needs this = the object |
| Prototype method | No | Yes | Method needs this = the instance |
| Constructor function | No | Yes | Arrows cannot be used with new |
| Class method | No | Yes | Class methods need dynamic this |
Event handler needing this | No | Yes | this should be the DOM element |
Event handler using event.target | Yes | Yes | Both work when not using this |
Function using arguments | No | Yes | Arrows have no arguments |
Function called with .call()/.apply() | No | Yes | Arrows ignore explicit this |
| Generator function | No | Yes | No arrow generator syntax exists |
Simple Rule
If the function needs its own this, its own arguments, needs to be a constructor, or needs to be a generator, use a regular function. For everything else, arrow functions are a great default.
Common Mistake Patterns
Class Method Defined as Arrow in Object Literal
// Bug in mixin/utility object
const loggerMixin = {
log: () => {
console.log(`[${this.name}] logged`); // this.name is undefined
},
};
// Fix
const loggerMixin = {
log() {
console.log(`[${this.name}] logged`);
},
};Arrow in Class Method That Gets Passed as Callback
class Counter {
count = 0;
// Arrow as class field: works, binds 'this' to instance
increment = () => {
this.count++;
};
// Regular method: 'this' lost when passed as callback
decrement() {
this.count--;
}
}
const c = new Counter();
const btn = document.getElementById("up");
// Works: arrow field keeps 'this'
btn.addEventListener("click", c.increment);
// Bug: 'this' becomes the button element
btn.addEventListener("click", c.decrement);
// Fix: bind or wrap
btn.addEventListener("click", () => c.decrement());
btn.addEventListener("click", c.decrement.bind(c));Arrow class fields are one situation where arrows intentionally bind this to the instance. This is a valid pattern in React class components and event handler setups.
Rune AI
Key Insights
- Never use arrows for object methods:
thiswill point to the enclosing scope, not the object - Arrows cannot be constructors: no
new, noprototypeproperty - Arrows ignore call/apply/bind: their
thisis permanently locked to the enclosing scope - Use rest parameters instead of arguments:
(...args)works in arrows and produces a real array - Arrow class fields are an exception: they intentionally bind
thisto the instance for callback use
Frequently Asked Questions
Can I use arrow functions as class methods?
Do arrow functions use more memory than regular functions?
If arrow functions cannot use arguments, are they less capable?
Is it bad to mix arrow and regular functions in the same file?
How do I know if a bug is caused by wrong this in an arrow function?
Conclusion
Arrow functions are the right choice for most callbacks, array methods, and short utility functions. They are the wrong choice when you need dynamic this binding (object methods, prototype methods, DOM event handlers that reference the element), when you need the arguments object, when you need a constructor, or when you need a generator. The key principle is simple: if the function needs its own this context, use a regular function. If it needs to inherit this from the surrounding scope, use an arrow function.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.