albert.walickihire me
← all solutions
javascript

Arrow functions vs regular functions in JavaScript

The real differences between arrow and regular functions in JavaScript: this, arguments, new, hoisting and object literals, and when to use each.

The main difference is this. An arrow function doesn't have its own this; it uses the this of the code around it, where it was defined. A regular function gets this from how it's called. Arrows also have no arguments, can't be used with new, and aren't hoisted like function declarations.

const counter = {
  count: 0,
  startRegular() {
    setTimeout(function () {
      console.log(this.count); // undefined
    });
  },
  startArrow() {
    setTimeout(() => {
      console.log(this.count); // 0
    });
  },
};

counter.startRegular();
counter.startArrow();

The regular callback is called by the timer, not by counter, so its this is something else (window in browsers). The arrow uses the this of startArrow(), which is counter. Before arrows, people wrote const self = this or .bind(this) to work around it.

Object methods written as arrows break

The flip side: don't write object methods as arrows. The arrow takes this from the code around the object, not from the object:

const user = {
  firstName: 'Ada',
  greet() {
    return `Hi, ${this.firstName}`;
  },
  greetArrow: () => `Hi, ${this.firstName}`,
};

user.greet();      // 'Hi, Ada'
user.greetArrow(); // 'Hi, undefined'

In an ES module, the top-level this is undefined, so greetArrow() throws a TypeError instead.

Event listeners

In a regular function listener, this is the element the listener is attached to. In an arrow, it isn't, so use event.currentTarget:

button.addEventListener('click', function () {
  this.classList.toggle('is-active');
});

button.addEventListener('click', (event) => {
  event.currentTarget.classList.toggle('is-active');
});

I prefer the second version, because it works the same no matter how the function is written.

No arguments, no new, no prototype

Arrows don't have their own arguments object. Inside an arrow, arguments refers to the enclosing regular function's arguments, which is rarely what you want. Use rest parameters, which work in both kinds of functions:

const sum = (...numbers) => numbers.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6

const User = (name) => ({ name });
new User('Ada'); // TypeError: User is not a constructor
User.prototype;  // undefined

You can't use an arrow as a constructor. For that, use a class.

Hoisting

Function declarations are hoisted, so you can call them before the line where they're defined. Arrows and function expressions assigned to const can't be used before that line:

greet('Ada'); // 'Hi, Ada'

function greet(name) {
  return `Hi, ${name}`;
}

shout('Ada'); // ReferenceError

const shout = (name) => `HI, ${name.toUpperCase()}`;

The error says "Cannot access 'shout' before initialization". This only bites when the call runs before the definition. Calling shout() inside another function that runs later is fine.

Returning an object literal

After =>, a curly brace starts a function body, not an object. Wrap the object in parentheses:

const toUserBroken = (name) => { name }; // returns undefined
const toUser = (name) => ({ name });     // returns { name }

Class fields as arrows

An arrow in a class field is created for each instance, with this fixed to that instance. It keeps working when you pass the method around as a callback, which is why it's common for event handlers:

class Toggle {
  isOn = false;

  handleClick = () => {
    this.isOn = !this.isOn;
  };
}

const toggle = new Toggle();
button.addEventListener('click', toggle.handleClick); // works

A normal method passed like that would lose its this. The trade-off is that each instance gets its own copy of the function instead of sharing one on the prototype.

Which one to use

  • Arrows for callbacks: map, filter, then, setTimeout, and event handlers that use event.currentTarget.
  • Method shorthand (greet() {}) for object and class methods.
  • class for anything you'd call with new.
  • For standalone named functions it's mostly style. I use function declarations for top-level helpers, because they're hoisted and easy to spot, and arrows everywhere else.
more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch