Declaring Functions

Mental model: A declaration is lifted whole to the top of its scope. An expression is just a value, and a value has to be assigned before you can use it.

Level: beginner · about 11 minutes

function squareA(n) { return n * n; }        // declaration
const squareB = function (n) { return n * n; }; // function expression
const squareC = (n) => n * n;                   // arrow function

console.log(squareA(5), squareB(5), squareC(5)); // → 25 25 25

Three ways to make the same function. Run it: all three log 25.

A declaration starts the statement with the keyword function and gives the function a name in the surrounding scope. A function expression appears where a value is expected, usually on the right of =. An arrow is a shorter expression form with a few deliberate omissions, covered in its own lesson.

Only declarations hoist as functions

Declaration: callable early

console.log(add(2, 3)); // 5

function add(a, b) {
  return a + b;
}

Expression: not yet

console.log(add(2, 3));
// ReferenceError: Cannot access
// 'add' before initialization

const add = (a, b) => a + b;

The declaration is registered with its body before the first line runs. The const is hoisted too, but into the temporal dead zone, so touching it early throws instead of silently giving you undefined. With var you get the worst version: undefined, then a TypeError when you try to call it.

console.log(typeof later);  // → 'undefined'  (the name exists, the value does not)

try {
  later();
} catch (err) {
  console.log(err.constructor.name); // → 'TypeError'  later is not a function
}

var later = function () { return 'here'; };

The var variant fails one step later, and less clearly.

Named function expressions

You can give a function expression a name. The name is not added to the outer scope; it is visible only inside the function body. That buys you two things: a useful entry in stack traces, and a way to call yourself without depending on the variable holding you.

const fact = function factorial(n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // the inner name always works
};

console.log(fact(5));            // → 120
console.log(fact.name);          // → 'factorial'
console.log(typeof factorial);   // → 'undefined'  not leaked outward

Calling it immediately

const config = (function () {
  const secret = 'not visible outside';
  return { mode: 'dark', hasSecret: secret.length > 0 };
})();

console.log(config);          // → { mode: 'dark', hasSecret: true }

// The arrow form of the same idea:
const total = (() => 2 + 3)();
console.log(total);           // → 5

fn.name and fn.length

function greet(greeting, name) {}
const withDefault = (a, b = 2) => {};
const withRest = (...args) => {};
const anon = function () {};

console.log(greet.length, greet.name);             // → 2 'greet'
console.log(withDefault.length);                   // → 1  stops at the first default
console.log(withRest.length);                      // → 0  rest never counts
console.log(anon.name);                            // → 'anon'  inferred from the variable

Two properties every function has. Note what length counts.

`fn.length`
the number of parameters before the first default or rest one
`fn.name`
the declared name, or one inferred from the variable it was assigned to
why it matters
libraries use both, for currying and for readable error messages
FormHoists callable?Has own this?Typical use
function f() {}yesyestop level helpers, methods
const f = function () {}no (TDZ)yeswhen you want the name pinned to a binding
const f = () => {}no (TDZ)nocallbacks, short transforms
(function () {})()runs immediatelyyesone off private setup
sayHi();
sayBye();

function sayHi() { console.log('hi'); }
const sayBye = () => console.log('bye');

sayHi is a declaration, so it is fully available before the first line. sayBye is a const holding an arrow, and it is still in its temporal dead zone at that point, so calling it throws ReferenceError: Cannot access 'sayBye' before initialization.

Try it yourself

Hoisting, three ways

console.log('declaration:', declared());

function declared() { return 'available before its line'; }

const expressed = function () { return 'needs its assignment first'; };
console.log('expression:', expressed());

const arrowed = () => 'same rules as the expression';
console.log('arrow:', arrowed());

console.log('names:', declared.name, expressed.name, arrowed.name);

Move each call above its definition, one at a time, and read the error. Then change const to var and notice how the failure gets less helpful.

Exercises

Describe a function

Write describeFn(fn) which returns an object { name, arity }. arity is the number of parameters the function declares before any default or rest parameter. When the function has no name, use the string '(anonymous)'.

Check yourself

What does this log?
'function' 'undefined' — The declaration one is hoisted with its body, so it is already a function. var two is hoisted as a name initialised to undefined, and the assignment has not run yet, so typeof two is 'undefined'.
What is the point of naming a function expression?
It gives you a name in stack traces and a reliable way to recurse from inside — The name is scoped to the function body only. Stack traces stop saying <anonymous>, and recursion no longer depends on the outer variable, which may be reassigned.
What does ((a, b = 1, ...rest) => {}).length evaluate to?
1 — length counts only the parameters before the first one with a default, and never counts a rest parameter. So it reports 1. Currying helpers use this to decide when they have collected enough arguments.

Common mistakes

  • Calling a function stored in a const or let above its declaration line, which throws instead of hoisting.
  • Assuming var hoisting makes a function expression callable early. The name exists, the value is undefined.
  • Trusting fn.length to be the total parameter count when defaults or rest are involved.

Takeaways

  • Declarations are hoisted with their body; expressions and arrows are values that must be assigned first.
  • A named function expression exposes its name only inside itself, which helps traces and recursion.
  • An IIFE is a scope you create and enter at once, and modules made it mostly unnecessary.
  • fn.length counts parameters up to the first default or rest one, and fn.name can be inferred.