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 25Three 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 variableTwo 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
| Form | Hoists callable? | Has own this? | Typical use |
|---|---|---|---|
function f() {} | yes | yes | top level helpers, methods |
const f = function () {} | no (TDZ) | yes | when you want the name pinned to a binding |
const f = () => {} | no (TDZ) | no | callbacks, short transforms |
(function () {})() | runs immediately | yes | one 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
oneis hoisted with its body, so it is already a function.var twois hoisted as a name initialised toundefined, and the assignment has not run yet, sotypeof twois'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) => {}).lengthevaluate to? - 1 —
lengthcounts only the parameters before the first one with a default, and never counts a rest parameter. So it reports1. Currying helpers use this to decide when they have collected enough arguments.
Common mistakes
- Calling a function stored in a
constorletabove its declaration line, which throws instead of hoisting. - Assuming
varhoisting makes a function expression callable early. The name exists, the value isundefined. - Trusting
fn.lengthto 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.lengthcounts parameters up to the first default or rest one, andfn.namecan be inferred.