Introduction
An arrow function is a concise, shorter syntax function in JavaScript introduced in ES6. You’ve likely heard of it because of its short syntax, which is what it’s most known for. But the shorter syntax is not the sole purpose of bringing it into JavaScript; instead, it was introduced to solve specific problems that couldn’t be addressed using regular functions.
Yes, I know many developers only know it is just limited to shorter syntax, but today I will explain this in much detail by putting it under the microscope.
Although we’ll start with its concise syntax, we’ll then go into its special part for which it was likely introduced. We’ll also learn the explicit difference between regular and arrow functions, which would clear our doubts that their behavior varies from regular functions.
Fasten your seat belt — it's going to be a bit of a long drive with me.
concise syntax
It is commonly used for its concise syntax, as I already mentioned. Many developers just know it up to here, and they use both of them interchangeably without knowing the difference. Hopefully, it doesn’t impact much most of the time due to its slightly subtle behavior, but you could get into trouble sometimes if you use it in an inappropriate place. I will mention this in the upcoming step. Let’s get back to the point:
// Regular function
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
Did you see what is meant by concise syntax? You can see the difference how the arrow function is written in one line compared to the regular function. Even though we can also write the regular function in one line, it would still work, but it wouldn’t seem appropriate, and it is also not considered good practice. You can check it below:
/// Traditional function
function add(a, b) { console.log(a + b); }
// Arrow function
const add = (a, b) => a + b;
As you can see, while it’s possible, it doesn’t look as clean as the arrow function. We can also omit the round brackets () in the arrow function if there is just one parameter.
const add = a => console.log(a);
add(3)
That is what it all is. Do you still think that arrow functions were just introduced for this purpose, 😆?
Not at all! It doesn’t make sense. As I showed you, we can achieve the inline thing with a regular function too, but with slightly less appropriate looks. That said, its structure is special, and it is known for its concise syntax.
I hope your doubt is clear about this point. Now we know the difference between regular and arrow functions. Since this is very important, we can now understand the real purpose it came into play for.
The real purpose of arrow functions
There is a significant difference between regular and arrow functions. After reading this article, you will clearly understand the distinction between them.
Not Hoisted
Arrow functions are not hoisted like regular functions. This means that a regular function can be called before it is defined, but an arrow function cannot — doing so will result in an error. If this sounds confusing, check the example below to understand what I mean.
// regular function
greet();
function greet() {
console.log("Hello"); // "Hello"
}
// arrow function
greet();
const greet = () => {
console.log("Hello"); // TypeError: greet is not a function
};
Have you noticed the difference between them? When we tried to call the arrow function greet() before defining it, it caused an error, but the regular function returned the expected output. You might be wondering: why does this happen? Let's find out.
When we create a regular function, it is hoisted. Hoisting in JavaScript is a behavior during the compilation phase where variable and function declarations are moved ("hoisted") to the top of their containing scope (either function or global scope) before the code is executed. This means the entire function definition is moved to the top of the scope during compilation. Therefore, we can call the function before its declaration in the code, and it will still work.
However, when we create an arrow function, it is not hoisted like a regular function. Arrow functions are usually assigned to variables (declared withconst, let, or var). Only the variable declaration is hoisted, not the assignment. In JavaScript, const and let are strict about usage they will throw a ReferenceError if you try to access the variable before it is declared.
console.log(test);
// ReferenceError: Cannot access 'name' before initialization
let test = 'hello';
console.log(test);
// ReferenceError: Cannot access 'name' before initialization
const test = 'hello';
But var behaves differently from both let and const. It is a loosely scoped and function-scoped variable declaration keyword, which is why its use is generally not recommended. When using var, the variable declaration is hoisted, allowing you to access the variable before its declaration although the value will be undefined until the assignment line is reached.
console.log(test);
// output: undefined
var test = 'hello';
Have you noticed? It returned the expected output: undefined. As we mentioned earlier, hoisting works for the variable declaration (test) but not for the assignment ('hello'). test is the variable name that was declared using var, while 'hello' is the value assigned to it. Even though we accessed the variable before its declaration, JavaScript didn't throw an error it simply returned undefined.
This happens because var is function-scoped and allows usage before declaration due to hoisting. However, since it behaves this way and lacks block scope, it is considered outdated and not recommended for use in modern JavaScript.
You can read a detailed article about var, let, and const here: Variable Scope In JavaScript.
You might be thinking I've gone off-topic but understanding these fundamentals is essential to see why var behaves differently than a regular function.
So it's clear: when we define a function using const or let, accessing it before declaration throws an error due to the lack of hoisting. But you may still wonder: why does it throw a TypeError even when we use var?
var sayHi; // sayHi is hoisted and initialized to undefined
sayHi(); // ❌ undefined is not a function
sayHi = () => { ... }; // assigned later
So when we called undefined() as a function, it throws a TypeError. For this reason, it always returns TypeError.
Lexical Behavior
Arrow functions work differently from regular functions because of something called lexical behavior. This means an arrow function doesn’t have its own this, arguments, super, or new.target. Instead, it uses these from the place where it was created.
I know this sounds a bit confusing, so I’ll explain it more simply.
Arrow functions are smaller and simpler than regular functions because they don’t have their own this, arguments, super, or new.target. Because of lexical behavior, arrow functions automatically use these from the surrounding code where they are written. This can make your code easier to write and understand.
Look at the example of a regular function:
function test(a,b,c) {
console.log(arguments); // output: [Arguments] { '0': 1, '1': 2, '2': 3 }
}
test(1,2,3);
Have you noticed that when we access arguments inside a regular function, it shows an array-like object containing all the arguments passed to that function? We often use this to work with the arguments. This means regular functions have their own arguments object. However, arrow functions do not have their own arguments object. Check out the example below:
const test = (a,b,c) => {
console.log(arguments); // ReferenceError: arguments is not defined
}
test(1,2,3);
Have you noticed that when we tried to access arguments in the arrow function, it threw an error? That's because arrow functions don’t have their own arguments object. Similarly, they lack other properties I’ve already mentioned related to traditional functions. That is why arrow functions exhibit lexical behavior—they inherit these features from their parent scope.
function outer(a,b,c) {
const arrow = () => {
console.log(arguments); // Inherits from 'outer'
};
arrow();
}
outer(1,2,3); // output: [Arguments] { '0': 1, '1': 2, '2': 3 }
Have you noticed that when we wrote an arrow function inside a regular function and tried to access arguments from the arrow function, it returned the expected output? Quite surprising, isn’t it?
You might be wondering why it returns arguments now, even though earlier it didn’t. You may ask: How can the arrow function access arguments if it doesn’t have its own?
The reason is that arrow functions have lexical behavior they don’t have their own arguments object but instead inherit it from their surrounding environment. In this case, the arrow function is enclosed within a regular function, which does have an arguments object. So the arrow function accesses that object through lexical scoping.
We used arguments in this example to clearly demonstrate the lexical behavior of arrow functions, and it helped us understand this concept. However, this alone doesn’t show the full value of arrow functions, because similar behavior can sometimes be achieved using regular functions.
To truly understand why arrow functions were introduced into JavaScript, we need to study their purpose in more detail. Then you will appreciate their real benefits.
arrow function with this
For this reason, the arrow function was introduced. As we know, arrow functions do not have their own this context; instead, they inherit this lexically from the surrounding scope. This behavior can seem unusual when working with the this keyword, and might make arrow functions seem less useful at first.
However, arrow functions were specifically designed to solve a common issue with this in JavaScript — where regular functions often require explicit binding (using .bind().), or saving a reference to this in a variable, to access the correct context inside callbacks or nested functions. By inheriting this from their defining scope, arrow functions simplify the handling of context in many situations.
I understand this might be a bit confusing at first, but I will explain it in detail so you can fully appreciate the true value of arrow functions.
Suppose we have an object.
const obj = {
firstName: 'John',
lastName: 'Harry',
fullName() {
console.log(this.firstName + " " + this.lastName);
}
}
obj.fullName(); // Output: John Harry
Did you notice? We have an object called obj, and within it, we have a few properties like firstName and lastName, along with a method named fullName, which is defined as a regular function. In JavaScript, a function defined inside an object is referred to as a method.
Inside the fullName method, I used this, which refers to the object (obj). So, this.firstName and this.lastName access the corresponding properties of the object. By joining these two properties, the method returns the full name — in this case, John Harry, as expected.
The this keyword refers to the object that is executing the current function. In this case, inside the method fullName, this refers to the obj object. You can see this behavior in the example below:
const obj = {
firstName: 'John',
lastName: 'Harry',
fullName() {
console.log(this);
}
}
obj.fullName(); // {firstName: 'John', lastName: 'Harry', fullName: ƒ}
Have you seen this pointed object? As I told you, I already have a well-detailed article on the this keyword. The purpose was to explain it to you so that you could become somewhat familiar with it, because having prior knowledge before fully understanding it is essential.
The problem 😨
Now, let's see what problem is solved by the arrow function.
Suppose we need to use a callback or asynchronous function inside an object that uses the this keyword. For example, if we want to delay the code — say, display the full name 3 seconds after the page loads — how would you achieve this? Obviously, like this:
const obj = {
firstName: 'John',
lastName: 'Harry',
fullName() {
setTimeout(function(){
console.log(this.firstName + " " + this.lastName); // undefined undefined
},3000)
}
}
obj.fullName();
// Output after 3 seconds: undefined undefined
But have you noticed that it returns undefined undefined after 3 seconds instead of the full name John Harry? Have you encountered this problem? Because of this issue, the arrow function was specifically introduced. You might be wondering: why does it return undefined undefined
The Solution 🚀
We know that a regular function has its own this context. So, in a method like fullName() inside an object, this correctly refers to the object (obj). Everything works as expected up to this point.
However, when we introduce a delay using the built-in setTimeout() function inside fullName, we encounter an issue: the this keyword inside the setTimeout callback no longer refers to the object (obj), but instead refers to the global object (window in browsers, or undefined in strict mode).
This happens because setTimeout executes the callback function in the global context, not in the context where it was defined. As a result, the callback’s this is not bound to the object. This occurs because regular functions do not have lexical binding of this; instead, their this value depends on how the function is called.
In contrast, arrow functions do not have their own this binding. Instead, they inherit this from the surrounding lexical scope. Therefore, when using an arrow function as the setTimeout callback, this retains the reference to the original object (obj), and the method works as expected.
const obj = {
firstName: 'John',
lastName: 'Harry',
fullName() {
setTimeout(() => {
console.log(this.firstName + " " + this.lastName); // John Harry
},3000)
}
}
obj.fullName();
// Output after 3 seconds: John Harry
So, due to its lexical scoping, it is very effective for this kind of problem, making it a valuable tool whenever we encounter similar issues.