DEV Community

Cover image for JavaScript Arrow Functions - Do you know how they work?
Matheus Julidori
Matheus Julidori

Posted on

JavaScript Arrow Functions - Do you know how they work?

TL;DR:

Arrow functions in JavaScript are concise, powerful, and perfect for callbacks and short expressions — but they behave differently from regular functions. They don’t have their own this, don’t get hoisted the same way, and are best used when you understand their scope. This post breaks down their syntax, context, hoisting behavior, and when to use them (or not). Ideal for beginners and anyone looking to truly understand how they work.


Arrow Functions are no longer a novelty in the world of JavaScript, but many people still don't know how they work and the differences between regular functions and arrow functions. Do you?

Introduction

JavaScript ES6+ introduced a bunch of new features, and one of them was Arrow Functions. But what exactly are they? The short answer is: functions, just like the ones declared using the function keyword. Let’s dive into this concept, starting with the syntax.

Arrow functions are defined as constants (using const is recommended, but let or var can also be used), followed by the arrow operator =>, as shown below. To call the function, the method is the same.

function addTwoNumbersAndDivideBy2(a, b) {
    const sum = a + b;
    return sum / 2;
}

const addTwoNumbersAndDivideBy2ArrowFunction = (a, b) => {
    const sum = a + b;
    return sum / 2;
}

console.log(addTwoNumbersAndDivideBy2(2, 8)) // 5
console.log(addTwoNumbersAndDivideBy2ArrowFunction(2, 8)) // 5 
Enter fullscreen mode Exit fullscreen mode

Above, we have the function to add two numbers written both ways. The syntax doesn’t change much, and the result is exactly the same, right? That’s because both take 2 arguments and perform two simple operations: addition and then division. But look what happens when there's only one operation, that is, when the function body only contains a return.

function subtractTwoNumbers(a, b){
    return a - b;
}

const subtractTwoNumbersArrowFunction = (a, b) => a - b;

console.log(subtractTwoNumbers(8, 2)); // 6
console.log(subtractTwoNumbersArrowFunction(8, 2)); // 6
Enter fullscreen mode Exit fullscreen mode

Now we can see a difference. When we have only one action, we can simplify the arrow function by removing the curly braces {} and omitting the return. This is one of the advantages of arrow functions over regular ones: readability and brevity.

But it goes even further. See how functions with just one argument can be defined:

function doubleNumber(a){
    return a * 2;
}

const doubleNumberArrowFunction = a => a * 2;
Enter fullscreen mode Exit fullscreen mode

Notice how we even omitted the parentheses around the parameter? That’s right—we simplified the function even more. Amazing! So I should start using arrow functions for everything since they're easier to write and read, and the result is always the same, right?

Well my friends, as the saying goes — not all that glitters is gold. Arrow functions may look simple, but looks can be deceiving.

Context and this

Arrow functions have some peculiarities. The first is the behavior of this. What does that mean in practice? Let's take a look.

const studentObj = {
    grade1: 10,
    grade2: 15,
    calculateAverage() {
        return (this.grade1 + this.grade2) / 2
    },
    calculateAverageArrow: () => (this.grade1 + this.grade2) / 2
};

console.log(studentObj.calculateAverage()) // 12.5
console.log(studentObj.calculateAverageArrow()) // NaN
Enter fullscreen mode Exit fullscreen mode

In this example, we have a student object with two grades and two functions to calculate the average: one regular and one arrow function. When we call the arrow function, the behavior isn’t what we expected. The average should be 12.5, but we get NaN. Why?

Let’s add another function to understand:

const studentObj2 = {
    grade1: 10,
    grade2: 15,
    calculateAverage() {
        return (this.grade1 + this.grade2) / 2
    },
    calculateAverageArrow: () => (this.grade1 + this.grade2) / 2,

    // New functions
    showGrade1() {
        return this.grade1;
    },
    showGrade1Arrow: () => this.grade1
};

console.log(studentObj2.showGrade1()) // 10
console.log(studentObj2.showGrade1Arrow()) // Undefined
Enter fullscreen mode Exit fullscreen mode

Now the issue is clear: context. A regular function, when executed, creates an execution context internally and inherits the parent object’s data. This context can be accessed using this.

Arrow functions, however, do not inherit the context from their parent object. So when we try to access this.grade1, it's undefined— hence the error.

In regular functions, the context of this is dynamic. If the function is executed as a method of an object, its context is the object. If it's executed in a standalone JS file running in a browser, the context is the window. In Node.js, the context is global.

Arrow functions behave differently. Their this always refers to the context in which they were defined, not executed. In technical terms, this is lexically scoped.

So, to determine the scope of an arrow function, always ask yourself: what is the surrounding context in which the function was defined?

Let’s run a few examples to understand this:

const arrow = () => console.log(this);

arrow(); 
Enter fullscreen mode Exit fullscreen mode

Assuming this code runs in a browser:

  1. Where was the arrow function declared? Inside the constant “arrow”.
  2. What is the context of that location? The browser’s window.
  3. What will be printed in the console? The browser’s window object.
const person = {
  name: "Matheus",
  regularMethod: function() {
    const arrow = () => console.log(this);
    arrow(); 
  }
};

person.regularMethod();
Enter fullscreen mode Exit fullscreen mode
  1. Where was the arrow function declared? Inside a regular function inside the person object.
  2. What is the context of that location? The person object.
  3. What will be printed in the console? The person object.
const person = {
  name: "Matheus",
  arrowMethod: () => console.log(this)
};

person.arrowMethod();
Enter fullscreen mode Exit fullscreen mode

Assuming this script runs in a Node.js environment:

  1. Where was the arrow function declared? Inside the person object.
  2. What is the context of that location? Node’s global.
  3. What will be printed in the console? Node’s global.

There are edge cases—like when using ES6+ strict mode—but that’s a topic for another time. The best way to determine the context is to debug it, but in most cases, the surrounding context question will work.

Hoisting

Another very important aspect is that arrow functions behave differently from regular functions in terms of hoisting. Let’s analyse:

/*
*
* Some code above
*
*
*/
console.log(doubleNumber(2)) // 4
/*
*
* Some code below
*
*
*/

function doubleNumber(a){
    return a * 2;
}
Enter fullscreen mode Exit fullscreen mode

In JavaScript, it's common to reference and use functions that are defined later in the code. If you come from a language like C++, this is similar to declaring a function prototype(signature) at the top, calling it in main, and only defining it at the bottom. In JavaScript, there’s no need for separate declarations—the interpreter hoists all function and variable declarations to the top. This is known as hoisting.

In the example above, even though doubleNumber is defined at the bottom, it works when called in the middle. But this doesn’t apply to arrow functions.

/*
*
* Some code above
*
*
*/
console.log(doubleNumberArrow(2)) // Uncaught Reference Error
/*
*
* Some code below
*
*
*/

const doubleNumberArrow = a => a * 2;
Enter fullscreen mode Exit fullscreen mode

Arrow functions don’t behave like regular functions when it comes to hoisting. “But Matheus, if the interpreter also hoists variables, why doesn’t it work?”
Because only function declarations are hoisted in full — both their name and body. Variables, on the other hand, are hoisted partially: only the name is moved to the top of the scope. The actual assignment (i.e., the function body) is not.
That means the variable doubleNumberArrow technically exists when it's called, but it hasn’t been initialized yet — and trying to access it results in a ReferenceError.

Anonymous Functions

Now that we’ve covered all of that, there’s still one key feature of arrow functions I consider their biggest advantage: anonymous function usage.

Anonymous functions are functions that are “defined” without being explicitly named. Sounds complex? Not really—check this out:

const numbers = [1, 2, 4, 8];
console.log(doubleNumbers(numbers)); // [2, 4, 8, 16]

function doubleNumbers(numbers) {
    return numbers.map(number => number * 2);
}
Enter fullscreen mode Exit fullscreen mode

The JS method array.map() expects a callback function that defines how to map the values. In this case, we want to multiply them by 2.
Instead of declaring a separate named function for doubling numbers, we use an arrow function inline, directly inside the parentheses. Notice how we wrote the arrow function without assigning it to any variable. That means it has no reference in memory and can’t be called later — making it an anonymous function. For this purpose, it’s perfect. It’s short-lived and exists only while map is running. Once map is done, the function is discarded and it will free memory space.

It’s worth noting that regular functions can also be anonymous, but they’re usually more verbose and less readable in these cases:

const numbers = [1, 2, 4, 8];
console.log(doubleNumbers(numbers)); // [2, 4, 8, 16]

function doubleNumbers(numbers) {
    return numbers.map(function (number){
        return number * 2;
    });
}
Enter fullscreen mode Exit fullscreen mode

So, Regular or Arrow?

That depends a lot on the developer. Here’s my personal recommendation.

I only use arrow functions in two situations:

  1. The first is shown above: as anonymous functions in callbacks.
  2. The second is in event listeners. Here is why:

Remember, callback functions don’t inherit the this of the parent element but instead get the context in which they were declared.

So in event listeners, this can be an advantage. Open a blank page (about:blank) and run the following in the console:

document.addEventListener("click", function(e){
    console.log(`Regular: this: ${this} target: ${e.target}`)
}) 

document.addEventListener("click", (e) => {
    console.log(`Arrow: this: ${this} target: ${e.target}`)
})
Enter fullscreen mode Exit fullscreen mode

When you click the page, you’ll see this output:

Regular: this: [object HTMLDocument] target: [object HTMLHtmlElement] 
Arrow: this: [object Window] target: [object HTMLHtmlElement]
Enter fullscreen mode Exit fullscreen mode

Notice how, in the regular function, this inherited the context of execution—i.e., the element that received the click. So e.target and this are the same.

But in the arrow function, e.target is still the same, but this refers to the general execution context—window. That gives us access to all elements of window within the event listener callback.

Wrapping Up

Arrow functions are one of those features in JavaScript that seem simple at first — and in many ways, they are. They make your code cleaner, more concise, and are especially handy for callbacks and one-liners. But as we’ve seen, they also come with important nuances around this, hoisting, and context that every developer should understand to avoid unexpected behavior. My advice? Use arrow functions where they shine, and regular functions where clarity and proper this binding are needed. As always, knowing when and why to use each tool is what truly makes the difference in writing great code.


💬 Have any cool use cases, questions, or arrow function gotchas you've run into?

Drop a comment below — I’d love to hear from you!

📬 Enjoyed this post? Follow me @matheusjulidori for more hands-on JavaScript tips, web dev insights, and fullstack content from a dev who loves teaching and good coffee. ☕🚀

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (2)

Collapse
 
nevodavid profile image
Nevo David

Really loved the straightforward breakdown of how arrow functions work!

Collapse
 
leonardo_galisse_ccbf84d8 profile image
Leonardo Galisse

That was a great reading.

To be honest i always struggle a little when it comes to arrow functions and regular, even in the React book I didn't quite grasp 100%. But your examples were spot on, nice job!

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay