Arrow Function vs Regular Function in LWC

In this post, we will explore Lightning Web Components (LWC), focusing specifically on the differences between regular functions and arrow functions.
You will learn:
- What regular and arrow functions are.
- The key differences between them.
- When to use an arrow function, and when a regular function is more appropriate within your LWC components.
It is also important to note that, in the context of Lightning Web Components, we often refer to these as methods rather than functions – but we will clarify this distinction as we progress.
Note: If your attention span is around two minutes, feel free to jump directly to the Cheat Sheet section, and don’t miss the Best Practices.
Let’s begin.
Function in JavaScript
A function is a standalone block of code that performs a specific task.
It is not tied to any object or class and can be called from anywhere within its scope.
According to the MDN Web Docs, functions in JavaScript can:
- Be passed to other functions.
- Be returned from functions.
- Be assigned to variables.
- Be assigned to object properties.
Additionally, functions:
- Can have their own properties.
- Can have methods.
- And most importantly, they can be invoked using syntax like
myFunction();
.
The key characteristic that distinguishes a function from other objects in JavaScript is that a function can be called.
In Lightning Web Components (LWC), functions are commonly used to organize shared JavaScript code. Typical examples include utils
, services
, or helper
modules.
Example: Utility Function
// utils.js function getSomething() { // ... }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { handleClick() { getSomething(); // Function invocation } }
Example: Function Defined Above a Class
You can also define functions outside of the class body, even within the same file:
function doSomething(param) { // ... }; export default class MyComponent extends LightningElement { handleClick() { doSomething(); // Function invocation } }
In this case, doSomething
is still a function, not a method, because it is not part of the MyComponent class.
Notice that we call it directly as doSomething()
, rather than this.doSomething()
, which would refer to a class method.
Method in JavaScript
“A method, like a function, is a set of instructions that perform a task. The difference is that a method is associated with an object, while a function is not.” ~ Codecademy [4]
A method is simply a function that is a property of an object or class.
It is designed to operate on data within the context of that object or class.
For example, in Lightning Web Components (LWC):
export default class MyComponent extends LightningElement { handleClick() { // This is a method console.log('Button clicked'); } }
In the example above, handleClick
is considered a method because it is defined inside a class (MyComponent
), making it part of that class’s behavior.
Methods in JavaScript Built-in Objects
Similarly, when working with arrays in JavaScript, you often use array methods.
These are called methods because they are functions associated with the Array object.
typeof []; // 'object' [1, 2, 3].find(x => x > 1); // Uses Array.prototype.find() [1, 2, 3].every(x => x > 0); // Uses Array.prototype.every() [1, 2, 3].filter(x => x !== 2); // Uses Array.prototype.filter()
These are not standalone functions; they are functions attached to the Array prototype, which makes them methods.
Function vs Method
Let’s summarize previous sections.
A function is not tied to any object or class, whereas a method is.
In LWC, any function defined inside a class or object is considered a method.
export default class MyComponent extends LightningElement { handleClick() { // This is a method console.log('Button clicked'); } }
Any standalone logic – especially shared utilities – is considered a function.
// utils.js function getSomething() { // This is a function // ... }; export { getSomething };
You might ask:
“Isn’t this just semantics? Are there any practical differences between methods and functions in LWC?”
This is exactly what we will explore next by comparing regular functions and arrow functions in more detail.
To provide a comprehensive view, we’ll extend the discussion to cover four scenarios:
- Regular function
- Arrow function
- Regular method
- Arrow method
For each case, we will evaluate the following aspects:
this
binding- The
arguments
object constructor
usage- Function hoisting
- Typical use cases
By the end of this post, this table will be fully populated:
Regular Function | Arrow Function | Regular Method | Arrow Method | |
---|---|---|---|---|
this binding |
– | – | – | – |
arguments object |
– | – | – | – |
constructor usage |
– | – | – | – |
Function hoisting | – | – | – | – |
Duplicate named parameters | – | – | – | – |
Use cases | – | – | – | – |
Strict Mode
Before we jump into the comparison, let’s briefly discuss strict mode in JavaScript.
Strict mode makes JavaScript safer and less error-prone. According to the MDN Web Docs:
Strict mode makes several changes to normal JavaScript semantics:
- Eliminates some JavaScript silent errors by changing them to throw errors.
- Fixes mistakes that make it difficult for JavaScript engines to perform optimizations: strict mode code can sometimes be made to run faster than identical code that’s not strict mode.
- Prohibits some syntax likely to be defined in future versions of ECMAScript.
Strict Mode in LWC
You don’t need to explicitly add "use strict" in your LWC code.
Both Lightning Web Security (LWS) and Lightning Locker implicitly enforce strict mode across all JavaScript executed within Salesforce. [8]
Why Is This Important?
In non-strict mode, if a function is called without a defined context, JavaScript will default this to the global object (window
in browsers):
function myFunc() { console.log(this); // Outputs: window (in non-strict mode) }
However, in strict mode, JavaScript refuses to default this to window.
If no context is provided, this becomes undefined
:
'use strict'; function myFunc() { console.log(this); // Outputs: undefined (in strict mode) }
This behavior is critical in LWC, where strict mode is always enabled by default.
As a result, this will never point to window, which protects your code from accidental access to the global scope.
We will explore how this behaves in various scenarios later in this post.
Duplicate named parameters
Another important rule enforced by strict mode is that duplicate parameter names are not allowed in function definitions.
For example:
function myFun(a, b, a) { console.log(a, b); };
Since LWC always operates in strict mode, this syntax will cause a parsing error in any function or method.
Example in LWC:
export default class MyComponent extends LightningElement { myFun(a, b, a) { // Error: Parsing error: Argument name clash. // ../ } }
Hoisting
As described in the MDN Web Docs:
JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables, classes, or imports to the top of their scope, prior to execution of the code.
What Does Hoisting Mean in Simple Terms?
In simple terms, hoisting means that JavaScript moves certain declarations to the top of their scope before the code runs.
This allows you to reference some functions or variables before they are defined in the source code.
Example – Function Hoisting
✅ This works because function declarations are fully hoisted:
sayHi(); function sayHi() { console.log('Hello!'); };
Even though sayHi()
is called before the function is defined, JavaScript still executes it because the function declaration is hoisted to the top of its scope during compilation.
Function Declaration vs Expression
Understanding the difference between a function declaration and a function expression is essential, especially when discussing hoisting and arrow functions.
Function Declaration
A function declaration is straightforward. It starts with the function keyword followed by a name for the function.
Function declarations are fully hoisted – both the function’s name and its body are available before the line where they are written.
This is why a regular function can be accessed before it is defined.
function sayHello() { console.log('Hi!'); };
To qualify as a function declaration, two criteria must be met:
- It must start with the
function
keyword. - It must include a function name.
If you write something like this:
const sayHello = function() { console.log('Hi!'); };
You’re using the function
keyword, but without a standalone declaration. This is known as a function expression.
Function Expression
If you search for “arrow function,” you’ll often find the term “arrow function expression” [12].
That’s because arrow functions are always function expressions.
A function expression occurs when a function is assigned to a variable (or constant) instead of being declared directly.
const sayHello = function() { console.log('Hi!'); };
const sayHello = () => { console.log('Hi!'); };
Both are function expressions because they involve assigning a function to a variable.
Hoisting Behavior
As stated in the MDN Web Docs:
Function expressions in JavaScript are not hoisted, unlike function declarations.
This means that while function declarations can be invoked before their definition, function expressions (including arrow functions) cannot.
Now that we understand the differences between functions and methods, what strict mode is, how hoisting works, and the difference between function declarations and function expressions, we can begin.
Regular Function
JavaScript Syntax
function functionName(param) { }; // or const functioName = function(param) { };
LWC Examples
Example 1 – Utility Function
// utils.js function getSomething() { // <== function // ... }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { handleClick() { getSomething(); // <== functions invocation } }
Example 2 – Function Defined Outside Class
function doSomething(param) { // <== function // ... }; export default class MyComponent extends LightningElement { handleClick() { // <== method doSomething(); // <== functions invocation } }
this
binding
Regular functions create their own this context, which is determined dynamically based on how the function is called.
What does this mean in practice?
Let’s slightly modify the previous example:
// utils.js function getSomething() { console.log(this); console.log(this.myProperty); }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { getSomething(); } }
In this case:
console.log(this.myProperty)
will outputundefined
.console.log(this)
will show aProxy(Function)
object.
Why?
in strict mode (which LWC enforces), this
inside a regular function called without a context is not window
. Instead, when calling a regular function inside an LWC component, Lightning Web Security (LWS) applies a Proxy to control access.
That Proxy is what appears as this
. It’s not your component or the global object (window
).
You can explicitly bind this
using .bind()
, .call()
, or .apply()
.
Let’s use this example:
// utils.js function doSomething(param1, param2, param3) { // ... console.log(this); console.log(this.myProperty); }; export { doSomething };
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { const param1 = 'A'; const param2 = 'B'; const param3 = 'C'; doSomething(param1, param2, param3); } }
As expected:
this
isProxy
this.myProperty
isundefined
How this will change with apply
, bind
and call
?
How It Changes with .apply()
, .bind()
, and .call()
?
.apply()
[…] calls this function with a given
this
value, andarguments
provided as an array [10]
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { const param1 = 'A'; const param2 = 'B'; const param3 = 'C'; doSomething.apply(this, [param1, param2, param3]); // <== apply } }
this
becomes the component instance, sothis.myProperty
will output “Hello, World!”.- The arguments are passed as an array:
[param1, param2, param3]
.
.bind()
[…] creates a new function that, when called, calls this function with its
this
keyword set to the provided value […] [9]
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { const param1 = 'A'; const param2 = 'B'; const param3 = 'C'; const newDoSomething = doSomething.bind(this); // <== bind newDoSomething(param1, param2, param3); } }
.bind(this)
doesn’t invoke the function immediately – it returns a new function withthis
permanently bound.this
refers to the component instance, sothis.myProperty
will output “Hello, World!”.
.call()
[…] calls this function with a given this value and arguments provided individually [11]
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { const param1 = 'A'; const param2 = 'B'; const param3 = 'C'; doSomething.call(this, param1, param2, param3); // <== call } }
this
becomes the component instance, sothis.myProperty
will output “Hello, World!”.- The difference between
.call()
and.apply()
is that with.call()
you pass arguments individually, whereas.apply()
expects an array of arguments.
arguments
object
As described in the MDN Web Docs:
The arguments object is an array-like object accessible inside functions that contains the values of the arguments passed to that function.
What Does This Mean?
In short, arguments
is an array-like object that is always accessible inside regular functions.
It contains all the values passed as parameters to the function.
Note: Although it behaves similarly to an array, arguments is not a true array. You cannot use array methods like .map()
, .filter()
, or .forEach()
on it directly.
function fun(a, b, c) { console.log(arguments[0]); // output: 1 console.log(arguments[1]); // output: 2 console.log(arguments[2]); // output: 3 } fun(1, 2, 3);
How Does This Apply in LWC?
You can access the arguments
object in regular functions within LWC as well:
// utils.js function doSomething() { console.log(arguments); // output: ['A', 'B', 'C'] }; export { doSomething };
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { handleClick() { doSomething('A', 'B', 'C'); } }
Even if the function does not explicitly define parameters, the arguments
object will still capture all passed values.
Why You Should Avoid Using arguments
?
Using arguments is generally considered bad practice, especially in modern JavaScript and LWC development:
- It’s not a real array – which limits functionality and can lead to confusion.
- It makes the code harder to read and maintain, as it hides the function’s expected parameters.
- It doesn’t work in arrow functions, and behaves inconsistently in modern patterns.
If you need to handle variable numbers of arguments, use the rest parameter syntax (...args
), which produces a true array:
❌ Avoid:
function doSomething() { console.log(arguments); }
✅ Preferred:
function doSomething(...args) { console.log(args); // [actual array] }
This approach is cleaner, more predictable, and compatible with array methods.
constructor
usage
Nothing complicated here: regular functions can be used as constructors.
✅
JavaScript
function Person(name) { this.name = name; } const me = new Person('Piotr'); console.log(me.name);
class Person { constructor(name) { this.name = name; } } const me = new Person('Piotr'); console.log(me.name);
LWC
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; constructor() { super(); } }
Hoisting
We’ve already discussed hoisting. To remind you, hoisting means that JavaScript moves declarations to the top of their scope before executing the code – allowing you to use certain functions or variables before they are defined.
✅
sayHi(); function sayHi() { console.log('Hello!'); };
Even though sayHi()
is called before the function is defined, JavaScript still runs it because the function declaration is hoisted to the top.
How Does This Apply in LWC?
In LWC, the behavior is the same for regular function declarations.
You can invoke a regular function even if it is declared below the call.
✅
// utils.js doSomething(); // Invoke function function doSomething() { console.log('doSomething'); }; function doSomethingElse() { console.log('doSomethingElse'); }; export { doSomething };
Use Cases
Shared Utility Logic
Use regular functions for general-purpose logic that can be reused across multiple components – such as formatting, calculations, string manipulation, etc.
// utils.js function formatCurrency(amount) { return `$${amount.toFixed(2)}`; }; function formatDate(dateString) { const options = { year: 'numeric', month: 'short', day: 'numeric' }; return new Date(dateString).toLocaleDateString(undefined, options); } export { formatCurrency, formatDate };
import { formatDate } from 'c/utils'; export default class MyComponent extends LightningElement { get formattedDate() { return formatDate('2024-04-16'); } }
It’s reusable, stateless, and doesn’t depend on this
or any component-specific context.
Pure Functions
Use regular functions when the output depends only on input parameters and has no side effects (i.e., it doesn’t modify external state).
// mathUtils.js function calculateDiscount(price, percentage) { return price - (price * (percentage / 100)); }; export { calculateDiscount };
Configuration & Mapping Helpers
Use regular functions for mapping codes, statuses, or creating dynamic labels.
// statusMapper.js function getStatusLabel(statusCode) { const statusMap = { NEW: 'New', IN_PROGRESS: 'In Progress', CLOSED: 'Closed' }; return statusMap[statusCode] || 'Unknown'; }; export { getStatusLabel };
Arrow Function
An arrow function is a type of function expression, which directly affects how it behaves regarding hoisting.
JavaScript Syntax
const myFunction = (param) => { // ... };
LWC Examples
Example 1 – Arrow Function in a Utility Module
// utils.js const getSomething = (param) => { // Arrow function // ... }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { handleClick() { getSomething(); // Arrow function invocation } }
Example 2 – Arrow Function Defined Above a Class
const getSomething = (param) => { // Arrow function // ... }; export default class MyComponent extends LightningElement { handleClick() { // Method doSomething(); // Arrow function invocation } }
this
binding
Arrow functions inherit their this
value from the surrounding lexical context.
In other words, they do not create their own this – instead, this
is inherited from the scope where the arrow function is defined.
What does this mean in practice?
With regular functions, Lightning Web Security (LWS) applies a Proxy
to the function context, which is why this may appear as Proxy(Function)
when invoked.
However, for arrow functions, LWS cannot intercept or rebind this
, because this
is lexically bound at the time the function is defined. This is why, when you define an arrow function in a separate module (like utils.js
), this will be undefined
.
In short: For arrow functions, this
is “locked in” based on the context where the function is defined – and it does not change, regardless of how or where the function is called.
// utils.js const getSomething = (param) => { // Arrow function console.log(this); console.log(this.myProperty); }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { getSomething(); // Arrow function invocation } }
In this case
console.log(this.myProperty)
will outputundefined
.console.log(this)
will also showundefined
.
This is effectively how the compiled code behaves:
const getSomething = param => { console.log(undefined); console.log(undefined.myProperty); };
As you can see, this
is locked to the context where the function was defined.
Since the top-level scope of utils.js
(in strict mode) has this as undefined, that’s what you get during execution.
You Cannot Rebind this in Arrow Functions
Another key difference between regular functions and arrow functions is that you cannot use .apply()
, .bind()
, or .call()
to change the value of this.
Why? Because, as mentioned, this is lexically bound and permanently set when the function is defined.
// utils.js const getSomething = (param) => { // Arrow function console.log(this); console.log(this.myProperty); }; export { getSomething };
import { getSomething } from "c/utils"; export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { const newGetSomething = getSomething().bind(this); newGetSomething(); // output: undefined // output: undefined } }
Even though you’re using .bind(this)
, it has no effect on an arrow function’s this
.
arguments
object
As stated in the MDN Web Docs:
The
arguments
object is a local variable available within all non-arrow functions.
How Does This Apply in LWC?
The behavior is similar in LWC.
In arrow functions, the arguments object is not available in the traditional sense.
Instead of being undefined
, attempting to access arguments in an arrow function within LWC returns a Proxy(Object)
due to Lightning Web Security (LWS).
However, this does not give you access to the actual passed arguments
// utils.js const doSomething = () => { console.log(arguments); // output: Proxy(Object) console.log(arguments[1]) // output: undefined }; export { doSomething };
import { doSomething } from "c/utils"; export default class MyComponent extends LightningElement { handleClick() { doSomething('A', 'B', 'C'); } }
As shown, even though you pass arguments to the function, you cannot retrieve them using arguments
in an arrow function.
Use ...args
Instead
If, for any reason, you need to handle dynamic arguments in an arrow function, use the rest parameter syntax (...args
). It provides a true array containing all passed values.
❌ Incorrect Approach:
const doSomething = () => { console.log(arguments); };
✅ Recommended Approach:
const doSomething = (...args) => { console.log(args); // [actual array] };
Using ...args
ensures predictable behavior and gives you full access to array methods like .map()
, .filter()
, etc.
constructor
usage
As stated in the MDN Web Docs [12]:
Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
[…]
Arrow functions cannot act as constructors because they do not have an internal [[Construct]] method, which is required by JavaScript to create new instances using the new keyword.
Example – Invalid Constructor Usage
❌ Attempting to use an arrow function as a constructor:
const Person = (name) => { this.name = name; }; const me = new Person('Piotr'); // TypeError: Person is not a constructor
❌ Attempting to define a class constructor as an arrow function:
class Person { constructor = (name) => { this.name = name; } }; const me = new Person('Piotr'); // Uncaught SyntaxError: Classes may not have a field named 'constructor'
In JavaScript, the constructor is a special method within classes, and it must follow standard function syntax. You cannot redefine it using an arrow function or as a class field.
Hoisting
Arrow function are always function expressions.
For example:
const sayHello = function() { console.log('Hi!'); };
const sayHello = () => { console.log('Hi!'); };
Both of these are function expressions because the function is assigned to a variable.
Now, let’s talk about arrow function hoisting.
As stated in the MDN Web Docs:
Function expressions in JavaScript are not hoisted, unlike function declarations.
Since an arrow function is always a function expression, it is not hoisted.
This means you cannot call an arrow function before its definition.
❌ Example – JavaScript
sayHello(); // Error: sayHello is not a function const sayHello = () => { console.log('Hi!'); };
Here, sayHello
is a const variable holding a function expression.
Variables declared with const
(or let
) are not initialized until their definition is evaluated.
So, calling sayHello()
before the assignment results in an error.
❌ Example – LWC module
// utils.js doSomething(); // this will cause error const doSomething = () => { console.log('doSomething'); }; const doSomethingElse = () => { console.log('doSomethingElse'); }; export { doSomething };
In this LWC module, calling doSomething()
before its declaration will throw an error because the arrow function is not hoisted.
Use Cases
Make It Shorter
If your function contains only a return
statement, you can simplify your code by using an implicit return:
❌
const add = (a, b) => { return a + b; }
✅
const add = (a, b) => a + b;
This approach is especially useful when working with JavaScript’s built-in array methods like .forEach()
, .map()
, .find()
, .filter()
, etc.
Here’s an example using an array of accounts:
const accounts = [ { Id: '0011x000003ABCD', Name: 'Acme Corporation', AnnualRevenue: 1500000 }, { Id: '0011x000003EFGH', Name: 'Global Media Inc.', AnnualRevenue: 2450000 }, { Id: '0011x000003IJKL', Name: 'GreenTech Solutions', AnnualRevenue: 980000 }, { Id: '0011x000003MNOP', Name: 'BrightPath Logistics', AnnualRevenue: 3750000 } ];
map
❌
const updatedAccounts = accounts.map(account => { return { ...account, isBigClient: account.AnnualRevenue > 100000 }; });
✅
const updatedAccounts = accounts.map(account => ({ ...account, isBigClient: account.AnnualRevenue > 100000 }));
find
❌
let currentAccountId = '0011x000003ABCD'; const currentAccount = accounts.find(account => { return account.Id === currentAccountId; });
✅
let currentAccountId = '0011x000003ABCD'; const currentAccount = accounts.find(account => account.Id === currentAccountId);
Promise Chains (then
, catch
, finally
)
Use arrow functions when handling asynchronous logic with Promises to keep your syntax clean and avoid this confusion.
fetchData() .then(result => { // Arrow function console.log('Data received:', result); }) .catch(error => { // Arrow function console.error('Error:', error); }) .finally(() => { // Arrow function // ... });
No need for dynamic this
binding inside promises. Keeps chaining readable.
setTimeout
To maintain this
context when using timers in your component logic.
❌
export default class MyComponent extends LightningElement { myProperty = 'Hello!'; startTimer() { setTimeout(function() { // Regular function console.log(this.myProperty) }.bind(this), 1000); } }
✅
export default class MyComponent extends LightningElement { myProperty = 'Hello!'; startTimer() { setTimeout(() => { // Arrow function console.log(this.myProperty); // No need for .bind(this) }, 1000); } }
Regular Method
A method is a function that is a property of an object or class.
The syntax is similar to a regular function, but for a function to be considered a method, it must be defined within an object or class.
JavaScript Syntax
class MyClass { myMethod() { // ... } }
LWC Examples
As you can see below, we don’t need to use function
keyword, to create a method in LWC.
export default class MyComponent extends LightningElement { handleClick() { // This is method // ... } }
this
binding
It’s not a surprise – this in a regular method follows the same rules as this in a regular function, meaning that this is determined dynamically, depending on how you call the method.
export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClick() { console.log(this); } }
When handleClick()
is called within the MyComponent
class, this refers to the component instance – displayed as Proxy(LightningElement)
due to Lightning Web Security (LWS).
You can safely access:
this.myProperty
this.template
this.dispatchEvent()
- And any other properties or methods defined in
MyComponent
or inherited fromLightningElement
.
Note:
LWS wraps the component instance in a Proxy for security, ensuring namespace isolation and controlled access. While this points to your component, what you see in the console is a secure proxy (Proxy(LightningElement)
), not the raw object. Importantly, this will never be undefined
or refer to a global object when properly invoked within the component context.
Losing this Binding
If you detach a method from its context, like this:
const clickHandler = this.handleClick; clickHandler();
The method loses its this
binding, because it’s no longer called as part of the component instance.
To fix this, you need to manually bind
, apply
or call
the method:
const clickHandler = this.handleClick.bind(this);
Example Binding Methods Dynamically
❌ Without .bind(this)
export default class MyComponent extends LightningElement { handleClick(e) { const actionToHandler = { save: this.save, remove: this.remove, cancel: this.cancel }; actionToHandler?.[e.detail.action]?.(); } save() { // ... console.log(this); // undefined } remove() { // ... console.log(this); // undefined } cancel() { // ... console.log(this); // undefined } }
In this example, when methods like save
or remove
are called via the object lookup, they lose their original this context, resulting in undefined
.
✅ With .bind(this)
export default class MyComponent extends LightningElement { handleClick(e) { const actionToHandler = { save: this.save.bind(this), remove: this.remove.bind(this), cancel: this.cancel.bind(this) }; actionToHandler?.[e.detail.action]?.(); } save() { // ... console.log(this); // Proxy(LightningElement) } remove() { // ... console.log(this); // Proxy(LightningElement) } cancel() { // ... console.log(this); // Proxy(LightningElement) } }
By using .bind(this)
, you ensure that this
inside each method still refers to the component instance, even when the methods are passed around as callbacks.
arguments
object
The arguments
object in a regular method works exactly the same as in a regular function:
- The
arguments
object is always accessible within a regular method. - It is an array-like object, but not a true array – you cannot use methods like
.map()
,.filter()
, or.forEach()
. - Using
arguments
is generally considered bad practice in modern JavaScript and especially in LWC development.
export default class MyComponent extends LightningElement { handleClick() { this.save('A', 'B', 'C'); } save() { console.log(arguments) // output ['A', 'B', 'C'] } }
In this example, the save method can access all passed parameters via the arguments
object, even though no parameters are explicitly declared.
Modern JavaScript offers better alternatives, like the rest parameter (...args
), which produces a true array.
export default class MyComponent extends LightningElement { handleClick() { this.save('A', 'B', 'C'); } save(...args) { console.log(args) // output ['A', 'B', 'C'] } }
This is clearer, more flexible, and works consistently across different function types.
constructor
usage
A regular method is the only valid way to define a constructor in LWC.
Since we are in the regular method section, it’s important to highlight how constructors work in JavaScript classes.
As explained in the MDN Web Docs:
The constructor method is a special method of a class used for creating and initializing an object instance of that class.
The constructor:
- Is a reserved method name.
- Can only be defined as a regular method within a class.
- Runs automatically when a new instance of the class is created.
In LWC, when defining a constructor, you must always call super()
first to ensure the base LightningElement
is properly initialized.
✅ Example:
import { LightningElement } from 'lwc'; export default class MyComponent extends LightningElement { myProperty; constructor() { super(); console.log('Constructor called!'); } }
Hoisting
Regular method hoisting technically doesn’t exist.
Hoisting applies to:
function fun() {}
- var declarations
In these cases, JavaScript moves the declarations to the top of their scope before execution, allowing them to be referenced earlier in the code.
However, when you define a regular method inside a class:
class MyClass { myMethod() { console.log('Hello'); } }
This method is not hoisted like a standalone function declaration.
Instead, JavaScript places myMethod on the class’s prototype, making it available after an instance of the class is created.
So, while it’s not “hoisted” in the traditional sense (like global or function-scoped declarations), it becomes accessible through instances of the class.
LWC example
export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; connectedCallback() { this.doAction(); } doAction() { console.log('Action'); } }
When LWC creates the component instance, this.doAction
refers to the method attached to the class prototype.
That’s why you can safely call regular methods within lifecycle hooks like connectedCallback()
– because by that point, the instance (and its prototype chain) is fully established.
Use Cases
Regular method:
- Belong to the component instance.
- Are placed on the prototype (shared across instances).
- Have dynamic
this
bound to the component when invoked properly.
Component Logic
Use regular methods for handling actions, processing data, or any internal operations tied to your component’s behavior.
export default class MyComponent extends LightningElement { accounts = []; handleLoadAccounts() { this.fetchAccounts(); } fetchAccounts() { console.log('Fetching accounts...'); } }
These actions are tied to the component’s lifecycle and state (this
).
Lifecycle Hooks
Lifecycle methods must be regular methods in LWC.
export default class MyComponent extends LightningElement { connectedCallback() { this.initializeComponent(); } initializeComponent() { console.log('Component initialized!'); } }
LWC framework expects standard method syntax for lifecycle hooks like connectedCallback
, renderedCallback
, etc.
Template Event Handlers
For handling user interactions from the template, like button clicks, input changes, etc.
<template> <lightning-button label="Click Me" onclick={handleClick}></lightning-button> </template>
export default class MyComponent extends LightningElement { handleClick() { console.log('Button clicked!'); } }
Internal Reusable Logic
For logic that will be called multiple times within your component but doesn’t need to be exposed outside.
export default class MyComponent extends LightningElement { handleSave() { if (!this.isValid()) { return; } this.save(); } isValid() { // ... return true; } save() { console.log('Data saved!'); } }
Public Methods (Exposed via @api)
When exposing methods to parent components.
import { api, LightningElement } from 'lwc'; export default class ChildComponent extends LightningElement { @api refreshData() { console.log('Data refreshed!'); } }
Only regular methods can be decorated with @api
for external access.
Overridable Methods in Extended Classes
When designing base components or shared logic using inheritance.
export default class BaseComponent extends LightningElement { logMessage() { console.log('Base message'); } } export default class ExtendedComponent extends BaseComponent { logMessage() { console.log('Extended message'); } }
Regular methods sit on the prototype chain, allowing clean overrides.
Wire Handlers
For handling logic inside @wire
decorated properties or functions.
import { LightningElement, wire } from 'lwc'; import getAccounts from '@salesforce/apex/AccountController.getAccounts'; export default class MyComponent extends LightningElement { @wire(getAccounts) wiredAccounts({ error, data }) { if (data) { console.log('Accounts:', data); } else if (error) { console.error(error); } } }
Wire service expects standard methods for function-style handlers.
Arrow Method
JavaScript Syntax
class MyClass { myMethod = () => { // ... } }
In this syntax, you’re using an arrow function assigned as a class property.
LWC Examples
When you define an arrow method like this in LWC:
export default class MyComponent extends LightningElement { sayHello = () => { console.log('Hello, World'); } }
You’re not defining a method in the traditional sense.
Instead, you’re defining a property on the instance and assigning it a function value – specifically, an arrow function.
Under the hood, this is equivalent to:
constructor() { this.sayHello = () => { console.log('Hello!'); }; }
Key Characteristics:
- It’s not part of the class prototype.
- The function is created during instance initialization, after the object is constructed (
new MyComponent()
). - Since it’s a property assignment (a function expression), there is no hoisting.
this
binding
As explained in the MDN Web Docs:
Arrow function expressions should only be used for non-method functions because they do not have their own
this
.
This means that arrow functions are not suited to be methods on objects.
"use strict"; const obj = { i: 10, b: () => console.log(this.i, this), // arrow function as method c() { console.log(this.i, this); // regular method }, }; obj.b(); // undefined, Window (or globalThis) obj.c(); // 10, { i: 10, b: ..., c: ... }
- b is an arrow function. It doesn’t have its own
this
. Instead, it inheritsthis
from the surrounding scope, which in this case is the global scope (window or globalThis). - c is a regular method, so this correctly refers to the obj.
How Does This Apply in LWC?
In LWC, arrow methods behave differently because of how classes handle them.
export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; handleClickRegular() { // Regular method console.log(this.myProperty); // this = component instance } handleClickArrow = () => { // Arrow method console.log(this.myProperty); // also works — this = component instance } }
So wait – why does the arrow method work in LWC even though MDN says not to use it as a method?
So, why does the arrow method work in LWC, even though MDN advises against using arrow functions as methods?
Because in a class, an arrow method is treated as a property assignment, not as a traditional method.
It’s defined as part of the instance during class instantiation, meaning the surrounding scope is effectively the class constructor.
In this context, this
refers to the component instance (Proxy(LightningElement)
).
Let’s review the example from Regular Method. We can change it to use arrow method.
Refactoring Example – Replacing Regular Methods with Arrow Methods
In the Regular Method section, you saw how methods could lose their this
binding if not handled properly. Here’s how you can refactor that using arrow methods to avoid manual .bind()
:
❌ Without Arrow Methods (Requires .bind())
export default class MyComponent extends LightningElement { handleClick(e) { const actionToHandler = { save: this.save, remove: this.remove, cancel: this.cancel }; actionToHandler?.[e.detail.action]?.(); } save() { // ... console.log(this); // undefined } remove() { // ... console.log(this); // undefined } cancel() { // ... console.log(this); // undefined } }
In this
case, this becomes undefined because the methods lose their binding when passed as references.
✅ Using Arrow Methods (No Need for .bind()
)
export default class MyComponent extends LightningElement { handleClick(e) { const actionToHandler = { save: this.save, remove: this.remove, cancel: this.cancel }; actionToHandler?.[e.detail.action]?.(); } save = () => { // ... console.log(this); // Proxy(LightningElement) } remove = () => { // ... console.log(this); // Proxy(LightningElement) } cancel = () => { // ... console.log(this); // Proxy(LightningElement) } }
With arrow methods:
this
is lexically bound at the time of definition.- No need to manually bind methods –
this
will always refer to the component instance. - Cleaner and safer when passing methods as callbacks or storing them in objects.
arguments
object
The arguments
object does not work in arrow methods.
Arrow functions, by design, do not have their own arguments object.
They inherit arguments from their surrounding non-arrow function context – and if there is none, arguments
is effectively unavailable.
Interestingly, in an LWC module (e.g., utils.js
), you can still deploy code where an arrow function references arguments:
// utils.js const doSomething = () => { console.log(arguments); // output: Proxy(Object) console.log(arguments[1]) // output: undefined }; export { doSomething };
Even though this doesn’t throw a syntax error, you cannot access the actual argument values.
Lightning Web Security (LWS) wraps arguments in a Proxy(Object)
, but it’s essentially useless in this context.
However, when you try to use arguments inside an arrow method within an LWC class:
export default class MyComponent extends LightningElement { handleClick() { this.save('A', 'B', 'C'); } save = () => { console.log(arguments); } }
You will encounter the following error at deployment:
"Parsing error: ‘arguments’ is only allowed in functions and class methods." and you won’t be able to deploy your code.
This happens because arguments is not allowed in arrow functions or arrow methods within classes.
If, for any reason, you need to handle a dynamic list of parameters in an arrow method, use the rest parameter syntax (...args
):
export default class MyComponent extends LightningElement { handleClick() { this.save('A', 'B', 'C'); } save = (...args) => { console.log(args); } }
constructor
usage
As stated in the MDN Web Docs [12]:
Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
[…]
In JavaScript, the constructor
is a special method within a class and must follow strict syntax rules.
You cannot define a constructor
using an arrow function.
❌ Invalid Example – Arrow Function as Constructor*
import { LightningElement } from 'lwc'; export default class MyComponent extends LightningElement { myProperty; constructor = () => { super(); console.log('Constructor called!'); } }
If you try to deploy an LWC component where the constructor is defined as an arrow function, you will receive the following error:
"Parsing error: Classes may not have a field named ‘constructor’."
This is because:
- The
constructor
must be declared using standard method syntax. - You cannot treat
constructor
as a class property or assign it as a function value. - Arrow functions are incompatible with constructor behavior since they lack the internal [[Construct]] method.
Hoisting
Arrow method hoisting technically doesn’t exist.
Regular method hoisting technically doesn’t exist.
Hoisting applies to:
function fun() {}
- var declarations
It does not apply to:
- Function expressions (including arrow functions)
- Class fields (like arrow methods)
- Variables declared with
let
orconst
When you define an arrow method in a class:
class MyClass { myMethod = () => { console.log('Hello, World'); } }
This is treated as a class field with a function assigned to it.
It’s simply a property assignment, so it is not hoisted like a function declaration.
Under the hood, JavaScript interprets it similarly to:
constructor() { this.myMethod = () => { console.log('Hello, World'); }; }
When JavaScript parses a class, it doesn’t “lift” class fields (including arrow methods) to the top of the class definition.
These properties are only assigned when the instance is created.
LWC example
export default class MyComponent extends LightningElement { myProperty = 'Hello, World!'; // this.doAction(); // You cannot do it connectedCallback() { this.doAction(); } doAction = () => { console.log('Action'); } }
In this example:
doAction
is an arrow method, assigned as a property during instance creation.- Since it’s assigned at runtime, you cannot call
this.doAction()
before the instance exists. - However, within lifecycle hooks like
connectedCallback()
, the instance is already initialized, so the method works as expected.
Note
Regular methods are better suited for reusable logic because they are placed on the class prototype, meaning they are shared across all instances.
In contrast, arrow methods are duplicated for each instance, which can impact performance, memory usage, and testability in large-scale applications.
Use Cases
Event Handlers Without .bind(this)
For event handlers where you might lose this
context – especially when dynamically passing handlers or using inline logic.
export default class MyComponent extends LightningElement { handleClick = () => { console.log(this.someProperty); // Always correctly bound } }
No need to worry about losing this
when the method is passed around.
Passing Methods as Callbacks
Use arrow methods when passing a method reference to another component or utility, where it will be invoked later. This ensures that this remains correctly bound to the component or class instance.
// buttons.js class CalculateHolidaysButton extends Button { handler = () => { // calculation } }; class CalculateSalaryButton extends Button { handler = () => { // calculation } }; const BUTTON_CONFIG = { HOLIDAYS: new CalculateHolidaysButton(), SALARY: new CalculateSalaryButton() }; function getButton(action) { return BUTTON_CONFIG[action]; }; export { getButton };
import { getButton } from 'c/buttons'; export default class MyComponent extends LightningElement { button; connectedCallback() { this.button = getButton('HOLIDAYS'); } }
<template> <lightning-button label="Click Me" onclick={button.handler}> </lightning-button> </template>
The arrow method ensures this
inside handler always points to the correct instance.
Dynamic Action Mappings
Use arrow methods when storing functions as values in objects, especially for dynamic action-handler mappings. This prevents this
from being lost when invoking these methods.
export default class MyComponent extends LightningElement { handleAction(event) { const actions = { save: this.save, cancel: this.cancel }; actions[event.detail.action]?.(); } save = () => { console.log('Save', this); } cancel = () => { console.log('Cancel', this); } }
This approach avoids issues with this
binding when methods are accessed dynamically from objects.
Cheat sheet
Feature | Regular Function | Arrow Function | Regular Method | Arrow Method |
---|---|---|---|---|
this binding |
Dynamic. Defaults to Proxy(Function) in LWC. Can rebind with .bind() , .call() , .apply() . |
Lexical. this is locked at definition. Cannot be rebound. Often undefined in module scope. |
Dynamic. Refers to Proxy(LightningElement) (component instance). |
Lexical. Always bound to component instance. Safe for callbacks & dynamic references. |
arguments object |
✅ Available (array-like, but not real array). | ❌ Not available. Use ...args instead. |
✅ Available. Same as regular function. | ❌ Not available. Use ...args instead. |
constructor usage |
✅ Can be used with new. | ❌ Cannot be used as constructor . |
✅ Special method constructor() . |
❌ Invalid. Cannot define constructor as arrow method. |
Hoisting | ✅ Fully hoisted (name + body). | ❌ Not hoisted. Must define before use. | Not hoisted, but available after instance creation (via prototype). | ❌ Not hoisted. Initialized per instance at runtime. |
Duplicate params | ❌ Disallowed (strict mode). | ❌ Disallowed. | ❌ Disallowed. | ❌ Disallowed. |
Use Cases | Utilities, helpers, stateless logic, reusable code across modules. | Callbacks, array methods, promises, concise inline logic. | Core component logic, lifecycle hooks, reusable methods tied to component behavior. | Event handlers, dynamic callbacks, avoid .bind(this) when passing methods around. |
Best Practices
Use Cases
- Use regular functions for:
- Shared utilities (
utils.js
, helpers, services). - Stateless, reusable logic independent of components.
- Pure functions and configuration mappers.
- Shared utilities (
- Use arrow functions for:
- Callbacks in
.map()
,.filter()
,.forEach()
. - Promise chains (
then
,catch
,finally
). - Inline, concise functions where lexical this is desired.
- Callbacks in
- Use regular methods for:
- Component logic and reusable internal methods.
- Lifecycle hooks (
connectedCallback
,renderedCallback
). - Public methods exposed via
@api
. - Logic where prototype sharing is beneficial.
- Use arrow methods for:
- Event handlers (
onclick
,onchange
), especially when passed around. - Dynamic action mappings to avoid losing
this
. - Callback references where binding would otherwise be needed.
- Event handlers (
Best Practices
Always specify list of arguments that are going to be passed to your method. If you have to use arguments
always use ...args
.
❌
// utils.js function doSomething() { console.log(arguments); } export { doSomething };
❌✅
// utils.js function doSomething(...args) { console.log(args); } export { doSomething };
✅
// utils.js function doSomething(a, b, c) { console.log(a, b, c); } export { doSomething };
Use arrow functions for concise callbacks in .map()
, .filter()
, Promise.then()
, etc.
✅
export default class MyComponent extends LightningElement { accounts = [ { Name: 'Acme', Revenue: 500000 }, { Name: 'GlobalTech', Revenue: 2000000 } ]; handleSort(revenue) const filteredAccountNames = this.accounts .filter(account => account.Revenue > revenue) .map(account => account.Name); } }
someAsyncCall() .then(result => { console.log('Success:', result); }) .catch(error => { console.error('Error:', error); });
Use regular methods for lifecycle hooks (connectedCallback
, renderedCallback
) and core component logic.
export default class MyComponent extends LightningElement { connectedCallback() { console.log('Component is inserted into DOM'); this.initializeData(); } initializeData() { // Core reusable logic console.log('Data initialized'); } }
Prefer regular methods for reusable internal logic to avoid per-instance duplication.
❌
export default class MyComponent extends LightningElement { calculateSum = (a, b) => a + b; }
✅
export default class MyComponent extends LightningElement { calculateSum(a, b) { return a + b; // Stored on prototype } handleClick() { console.log(this.calculateSum(5, 10)); } }
Resources
[1]
Arrow Functions vs Regular Functions in JavaScript – What’s the Difference?[2]
Functions[3]
Method vs Functions, and other questions[4]
Methods and Functions[5]
Lightning Locker[6]
Lightning Web Security[7]
Strict mode[8]
JavaScript Strict Mode Enforcement[9]
Function.prototype.bind()[10]
Function.prototype.apply()[11]
Function.prototype.call()[12]
Arrow function expressions[13]
The arguments object[14]
Hoisting[15]
function expression
Note: This post was not written by AI. I only used AI to help refine my grammar because I am not a native speaker.