When to Avoid Using Arrow Functions in JavaScript

Arrow functions are concise and useful, but they are wrong for object methods, constructors, event handlers that need this, and a few other cases. Learn when to reach for a regular function instead.

6 min read

Arrow functions are concise, elegant, and everywhere in modern JavaScript. But they are not a drop-in replacement for regular functions. They have deliberate limitations, no own this, no arguments object, no prototype, that make them the wrong choice in specific situations.

Using an arrow function where a regular function belongs leads to undefined values, silent failures, or runtime errors. This article covers the cases where you should reach for a regular function instead of an arrow.

1. Object Methods

Arrow functions should not be used as object methods. They do not have their own this, so this inside the method refers to the enclosing scope, not the object:

javascriptjavascript
const user = {
  name: "Alex",
  greet: () => {
    console.log(`Hi, I'm ${this.name}`);
  },
};
 
user.greet(); // "Hi, I'm undefined"

The fix is the method shorthand syntax, which correctly binds this to the object because regular methods receive their own this based on how they are called, not where they were defined:

javascriptjavascript
const user = {
  name: "Alex",
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  },
};
 
user.greet(); // "Hi, I'm Alex"

This applies to methods in objects, classes, and prototypes. If you need this to refer to the calling object, use a regular function or method shorthand.

2. Constructors

Arrow functions cannot be used as constructors. They lack a prototype property and throw a TypeError when called with new:

javascriptjavascript
const Person = (name) => {
  this.name = name;
};
 
const p = new Person("Alex"); // TypeError: Person is not a constructor

Regular functions and classes are the correct tools for creating objects with new, since they have the internal machinery the new keyword depends on:

javascriptjavascript
function Person(name) {
  this.name = name;
}
 
const p = new Person("Alex");
console.log(p.name); // "Alex"

3. Event Handlers That Need this

In DOM event handlers, this normally refers to the element that fired the event. Arrow functions break that behavior:

javascriptjavascript
button.addEventListener("click", function() {
  console.log(this); // The button element
  this.classList.toggle("active"); // Works correctly
});
 
button.addEventListener("click", () => {
  console.log(this); // The enclosing scope (window or module)
  this.classList.toggle("active"); // TypeError or wrong element
});

If your handler needs this to be the event target, use a regular function. If you do not need this, arrows are fine, but be explicit about why you chose one over the other.

4. When You Need the arguments Object

Arrow functions do not have the arguments object. If you reference arguments inside an arrow function, you get the enclosing function's arguments instead:

javascriptjavascript
function outer() {
  const inner = () => {
    console.log(arguments); // outer's arguments, possibly unexpected
  };
  inner("inner-arg");
}
 
outer("outer-arg"); // ["outer-arg"], not ["inner-arg"]

Use rest parameters when you need variable arguments in an arrow function. A rest parameter collects every argument into a real array with none of the ambiguity the arguments object introduces:

javascriptjavascript
const logAll = (...args) => {
  console.log(args);
};
 
logAll(1, 2, 3); // [1, 2, 3]

But if you specifically need the arguments object, for example in a function that must interface with legacy code, use a regular function.

5. Recursive Named Functions

Arrow functions are always anonymous. You can assign them to a variable and call that variable recursively, but you cannot reference the function by its own name:

javascriptjavascript
// This works (variable self-reference)
let factorial = (n) => {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
};
 
// But if the variable is reassigned, it breaks
const oldFactorial = factorial;
factorial = null;
oldFactorial(5); // TypeError: factorial is not a function

For recursion, a named function expression or declaration is safer, because the internal name always points to the original function regardless of what happens to the outer variable:

javascriptjavascript
const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1); // Uses internal name, safe from reassignment
};

6. Generator Functions

Arrow functions cannot be generators. There is no arrow syntax equivalent to function*, so the yield keyword has no valid meaning inside an arrow function body:

javascriptjavascript
// SyntaxError
const generator = () => {
  yield 1;
};

Use a regular generator function instead, adding an asterisk after the function keyword to mark it as a generator that can yield values:

javascriptjavascript
function* generator() {
  yield 1;
  yield 2;
}

7. Functions Added to a Prototype

When adding methods to a prototype, this needs to refer to the instance. Arrow functions will not work:

javascriptjavascript
function Counter() {
  this.count = 0;
}
 
// Wrong: arrow function, this is not the instance
Counter.prototype.increment = () => {
  this.count++;
};
 
// Correct: regular function, this is the instance
Counter.prototype.increment = function() {
  this.count++;
};

8. When call, apply, or bind Is Needed

Arrow functions ignore call, apply, and bind. Their this is fixed at definition time and cannot be overridden at call time:

javascriptjavascript
const add = (a, b) => this.value + a + b;
 
const obj = { value: 100 };
console.log(add.call(obj, 1, 2)); // NaN -- this is not obj

If you need to dynamically set this, use a regular function.

Decision Guide

With eight separate cases to remember, it helps to collapse them into a single yes-or-no path. The flow below walks through the questions that matter most, in the order that eliminates arrow functions fastest:

Arrow function vs regular function decision

The diagram above summarizes the decision flow. If you need this, the arguments object, new, yield, or prototype methods, use a regular function. For everything else, arrow functions are the cleaner choice.

For the basics of arrow function syntax, see JavaScript Arrow Functions: A Complete ES6 Guide. For the broader comparison with regular functions, see JavaScript Function Expressions vs Declarations.

Rune AI

Rune AI

Key Insights

  • Do not use arrow functions for object methods -- this will not point to the object.
  • Arrow functions cannot be constructors and throw TypeError when called with new.
  • Do not use arrow functions as event handlers that need this to be the event target.
  • Arrow functions lack arguments, prototype, and cannot be generators.
  • Use regular function declarations for methods, constructors, and recursive named functions.
RunePowered by Rune AI

Frequently Asked Questions

Can I mix arrow and regular functions in the same codebase?

Yes, and most codebases do. Use arrow functions for short callbacks and array methods. Use regular functions for methods, constructors, and places where you need dynamic this.

Are arrow functions faster than regular functions?

The performance difference is negligible in modern engines. Choose based on semantic correctness, not micro-optimization.

Should I use arrow functions in React class components?

If you still use class components, arrow functions as class fields can be useful for auto-binding methods. But with function components and hooks, this is no longer a concern.

Conclusion

Arrow functions are a great tool, but they are not a universal replacement for regular functions. Avoid them for object methods, constructors, dynamic this patterns, and when you need the arguments object or the prototype chain. Knowing when not to use them is just as important as knowing the syntax.