What you'll be able to do
By the end of this page you should be able to spot a function declaration and a function expression, know when you can call a function before its line in the file, and avoid the “Cannot access before initialization” surprise with const / let expressions.
Previous post: functions from zero.
Who this is for
- People who saw
function greet() {}andconst greet = function () {}and thought they were the same in every way - Beginners who call a function at the top of a file and get an error only after switching to
const - Anyone confused by the word hoisting
You can skip this if you already know declarations are hoisted as callable, expressions assigned with const are not, and you pick a style on purpose. Come back when a refactor suddenly breaks “call order.”
Shape 1: function declaration
A declaration starts with the function keyword and a name:
function add(a, b) {
return a + b;
}
console.log(add(2, 3));
Scenario A — helpers at the bottom of a script. Many beginners put function blocks at the bottom and call them from the top. With declarations, that often works.
Scenario B — named for stack traces. When something throws, the error often shows add as the function name, which helps debugging.
Shape 2: function expression
A function expression creates a function value and usually stores it in a variable:
const add = function (a, b) {
return a + b;
};
console.log(add(2, 3));
The function on the right is an expression. const add = ... is the binding.
Scenario A — same behavior when called after the line. Once add exists, add(2, 3) works like the declaration version.
Scenario B — assigning later or conditionally. Expressions fit “create this function value and put it here,” including patterns you will see with callbacks later.
You can also name the expression: const add = function add(a, b) { ... }. The inner name is useful for recursion and clearer stacks; beginners can ignore that until they need it.
Hoisting — honest beginner version
Hoisting means the engine sets up declarations before running your lines top to bottom. It is not that your file is secretly rearranged into a new document you never wrote — it is how the engine prepares names.
Declarations: a function add() {} declaration is available for calling through the whole scope, even above its line:
console.log(double(4)); // 8
function double(n) {
return n * 2;
}
Scenario A — call above declaration. Works with function declarations.
Expressions with const / let: the variable exists in a “temporal dead zone” until the line runs. You cannot call it before that line:
// console.log(triple(4)); // Error if uncommented
const triple = function (n) {
return n * 3;
};
console.log(triple(4)); // 12
Scenario B — you “converted” a declaration to const and kept a call at the top. Suddenly ReferenceError. The fix is move the call below the const, or keep a declaration if you want top-of-file calls.
var function expressions hoist the variable as undefined, not as a callable function — so fn() before the assignment still fails. Prefer const and call after the assignment; do not lean on var tricks.
Which one should you use?
Both are valid. Pick for clarity, not fashion.
Scenario A — small script, helpers below. Declarations are fine and friendly for beginners.
Scenario B — modern modules and const style. Many codebases prefer const greet = function () {} or arrows (next post). Then always define before call in that file order.
Rule of thumb: if you use const/let for functions, treat them like any other variable — create first, use second.
A tiny practice file
Save as decl-vs-expr.html. Click to run both shapes; the page also shows a safe call order note.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Declaration vs expression</title>
</head>
<body>
<button id="go">Run</button>
<pre id="out">Result will show here.</pre>
<script>
function declaredAdd(a, b) {
return a + b;
}
const expressedAdd = function (a, b) {
return a + b;
};
document.getElementById("go").addEventListener("click", function () {
const lines = [];
lines.push("declaration add(2, 3): " + declaredAdd(2, 3));
lines.push("expression add(2, 3): " + expressedAdd(2, 3));
lines.push("Call const functions only after their line.");
document.getElementById("out").textContent = lines.join("\n");
});
</script>
</body>
</html>
In the console, try calling expressedAdd(1, 1) above its const line in a scratch file — you should see the TDZ error. That is the lesson.
Mistakes I see a lot
1. Assuming every function can be called from line 1. Only declarations (in that scope) behave that way.
2. Mixing var fn = function and thinking it is fully hoisted as a function. The variable is undefined until assigned.
3. Two functions with the same name. A later declaration can overwrite an earlier one in the same scope. Prefer one clear name.
4. Thinking expressions are “not real functions.” They are. Only the binding style differs.
5. Refactoring declaration → const without moving call sites. Breaks at runtime.
What to try before the next post
- Call a declaration above its line; confirm it works.
- Put a
constexpression below a call and watch the error. - Rewrite one helper both ways; keep behavior the same.
- Build the tiny HTML file.
Next in this series: arrow functions — when to use them — the short => form, and when a normal function is still clearer (before a deep dive on this).
Try this next outside the series
Functions matter more once they stop being textbook examples and start shaping reusable app code.
- Node.js async patterns — see functions return promises, accept callbacks, and coordinate real work
- Diff Checker — compare two function versions before you keep a refactor