JavaScript Function Declarations vs Expressions vs Arrows: When to Use Which

Link copied
JavaScript Function Declarations vs Expressions vs Arrows: When to Use Which

JavaScript Function Declarations vs Expressions vs Arrows: When to Use Which

JS Tutorial Module 2: Functions Lesson 2.1

JavaScript has three ways to make a function: the declaration, the expression, and the arrow. They look superficially similar — three different keyword shapes for the same thing — but they differ in hoisting, in this binding, in whether they can be used as constructors, and in subtle parser-level rules. Knowing which to pick is one of those small decisions that affects every file you write.

This lesson walks through each form, the technical differences, and the practical rules of thumb modern codebases use.

The three forms side by side #

// 1. Function declaration
function add(a, b) { return a + b; }

// 2. Function expression
const add = function (a, b) { return a + b; };
const add = function adder(a, b) { return a + b; }; // named expression

// 3. Arrow function
const add = (a, b) => a + b;

All three produce a callable function. They differ in five places: hoisting, this, arguments, constructibility, and self-reference.

Difference 1: hoisting #

Declarations are fully hoisted. The function exists and is callable from anywhere in its enclosing scope, including lines above the declaration:

add(2, 3);  // works
function add(a, b) { return a + b; }

Expressions and arrows assigned to const/let are hoisted as bindings but live in the TDZ until their assignment line. Calling them earlier throws (we covered this in Lesson 1.2):

add(2, 3);  // ReferenceError
const add = (a, b) => a + b;

For var (which you shouldn't use), the variable is hoisted as undefined, so calling earlier throws TypeError: add is not a function.

This is why top-level utility functions in a file are often written as declarations — they're available everywhere, regardless of order.

Difference 2: this binding #

Declarations and traditional expressions have their own this. It's set by the caller — by obj.method() (it's obj), by new Func() (it's the new instance), by func.call(thisArg) (explicit), or by func() (undefined in strict, global in sloppy mode).

Arrows do not have their own this. They inherit it lexically from the surrounding scope.

const obj = {
  name: 'Ada',
  greetRegular: function () { return 'Hi ' + this.name; },
  greetArrow:   ()         => 'Hi ' + this.name,
};

obj.greetRegular(); // 'Hi Ada' — `this` is `obj`
obj.greetArrow();   // 'Hi undefined' — `this` is the outer scope's

For object methods, you want the regular form (or method shorthand). For callbacks, you usually want the arrow form because the surrounding this is exactly what you need:

class User {
  constructor(name) {
    this.name = name;
  }
  delayedGreet() {
    // With arrow — `this` is the User instance
    setTimeout(() => console.log('Hi ' + this.name), 1000);

    // With function — `this` is something else (undefined here)
    // setTimeout(function () { console.log(this.name); }, 1000); // BUG
  }
}

This single difference is why arrows took over for callbacks — they sidestep the this rebinding gotcha.

Lesson 2.6 covers this in full.

Difference 3: arguments #

Regular functions get an automatic arguments object — array-like, contains all passed arguments. Arrows do not.

function sumOld() {
  let total = 0;
  for (const n of arguments) total += n;
  return total;
}

const sumArrow = () => arguments; // ReferenceError or inherited from outer scope

The modern replacement (works in any form): rest parameters.

const sum = (...nums) => nums.reduce((s, n) => s + n, 0);

Rest gives you a real array, works in arrows, and is the recommended modern style. The arguments object is largely a legacy concern at this point.

Difference 4: constructibility #

Declarations and traditional expressions can be used with new:

function Person(name) { this.name = name; }
const p = new Person('Ada');

Arrows cannot. new (() => {}) throws.

In modern code, this rarely matters — for constructors, you'd use class syntax (Lesson 4.2). The arrow restriction just confirms arrows are for callbacks and helpers, not for constructors.

Difference 5: self-reference (named function expressions) #

A named function expression has a name accessible only inside the function:

const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);  // can call itself
};

factorial(5);  // 120
typeof fact;   // 'undefined' — only visible inside

Arrows have no name binding of their own. For recursion in an arrow, you reference the variable it was assigned to:

const factorial = (n) => n <= 1 ? 1 : n * factorial(n - 1);

Mostly works. The fragile case: if someone reassigns factorial, the recursion breaks. Named function expressions are immune to that — the inner name is bound to the function itself.

In practice, this is a curiosity. Most recursive functions are named declarations.

The full comparison #

Aspect Declaration Expression Arrow
Hoisted as callable Yes No (TDZ) No (TDZ)
Own this Yes Yes No (inherits)
Has arguments Yes Yes No
Usable with new Yes Yes (if not arrow) No
Can have a name visible inside Yes (its declared name) Yes (if named) No
Implicit return for single expressions No No Yes
Concise syntax for callbacks No Verbose Yes

Modern rules of thumb #

In 2026, most teams converge on these:

  1. Top-level utility functions → declarations. function processOrder(...) {}. Hoisted, stack-trace-friendly, easy to grep for.
  2. Class methods → method shorthand (greet() {}). Same as a function expression but cleaner.
  3. Callbacks, inline functions, short helpers → arrows. .map(n => n * 2), setTimeout(() => log('hi'), 100).
  4. Functions used as methods on plain object literals → regular shorthand methods. Not arrows (they lose this).
  5. Constructorsclass syntax. Don't define them via function.

Let ESLint or Prettier enforce the style. ESLint's prefer-arrow-callback is the most common one.

A worked file #

Here's the shape of a typical modern module:

// scripts/process-orders.js

// Top-level utilities — declarations.
function parseAmount(raw) { return Number(raw); }
function validateOrder(order) { /* ... */ }

// Constants — arrow if they're functions, plain values otherwise.
const now = () => Date.now();
const FIVE_MINUTES = 5 * 60 * 1000;

// Class — modern syntax.
class OrderProcessor {
  process(order) {
    // Method body. `this` is the instance.
    const validated = validateOrder(order);
    return validated.map(item => parseAmount(item.price));
  }
}

// Module-level main — arrow.
const main = async () => {
  const orders = await fetchOrders();
  const proc = new OrderProcessor();
  return orders.map(o => proc.process(o));
};

main().catch(err => console.error(err));

Different forms for different purposes. The result reads cleanly because each function's shape matches its role.

What's next #

Lesson 2.2 covers parameters and arguments — default values, rest parameters, destructuring, and the patterns that turn cluttered function signatures into self-documenting ones.

Try it yourself #

The hoisting difference is the easiest to feel. Predict which line throws and which one works:

YouPredict the output:
declared(); // line A
function declared() { console.log('declared'); }

try { expressed(); } // line B
catch (e) { console.log('B threw:', e.name); }
const expressed = () => console.log('expressed');
Claude · used js_sandboxOutput:
declared (line A works — function declaration is fully hoisted)
B threw: ReferenceError (line B fails — expressed is in the TDZ until its const line)

This is exactly why top-level utility functions tend to be written as declarations — you can use them anywhere in the file regardless of order.

Declaration vs arrow isn't a question of taste alone — they behave differently. Pick based on what your code needs, not what looks shortest.

Up next in JavaScript

More from this topic

View all JavaScript articles →

Enjoyed this article?

Get new JavaScript tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email address will not be published. Required fields are marked *