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 ownthis
, 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
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
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;
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
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
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();
Assuming this code runs in a browser:
- Where was the arrow function declared? Inside the constant “arrow”.
-
What is the context of that location? The browser’s
window
. -
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();
-
Where was the arrow function declared? Inside a regular function inside the
person
object. -
What is the context of that location? The
person
object. -
What will be printed in the console? The
person
object.
const person = {
name: "Matheus",
arrowMethod: () => console.log(this)
};
person.arrowMethod();
Assuming this script runs in a Node.js environment:
-
Where was the arrow function declared? Inside the
person
object. -
What is the context of that location? Node’s
global
. -
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;
}
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;
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);
}
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;
});
}
So, Regular or Arrow?
That depends a lot on the developer. Here’s my personal recommendation.
I only use arrow functions in two situations:
- The first is shown above: as anonymous functions in callbacks.
- 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}`)
})
When you click the page, you’ll see this output:
Regular: this: [object HTMLDocument] target: [object HTMLHtmlElement]
Arrow: this: [object Window] target: [object HTMLHtmlElement]
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. ☕🚀
Top comments (2)
Really loved the straightforward breakdown of how arrow functions work!
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!