Arrow Function vs Regular Function in LWC

arrow function 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:

  1. Regular function
  2. Arrow function
  3. Regular method
  4. 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:

  1. Eliminates some JavaScript silent errors by changing them to throw errors.
  2. 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.
  3. 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:

  1. It must start with the function keyword.
  2. 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 output undefined.
  • console.log(this) will show a Proxy(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.

PROXY Function lwc

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 is Proxy
  • this.myProperty is undefined

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, and arguments 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, so this.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 with this permanently bound.
  • this refers to the component instance, so this.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, so this.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 output undefined.
  • console.log(this) will also show undefined.

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 a TypeError[…]

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 from LightningElement.

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 inherits this 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 a TypeError[…]

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 or const

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.
  • Use arrow functions for:
    • Callbacks in .map(), .filter(), .forEach().
    • Promise chains (then, catch, finally).
    • Inline, concise functions where lexical this is desired.
  • 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.

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


Note: This post was not written by AI. I only used AI to help refine my grammar because I am not a native speaker.

Piotr Gajek
Piotr Gajek
Senior Salesforce Developer
Technical Architect and Full-stack Salesforce Developer. He started his adventure with Salesforce in 2017. Clean code lover and thoughtful solutions enthusiast.

You might also like

Promises in LWC
September 4, 2022

Promises in LWC

What is a Promise? How does it work in LWC? What is the difference between then/catch and async/await? Let’s check.

Piotr Gajek
Piotr Gajek

Senior Salesforce Developer