JAVASCRIPT / VARIABLES: VAR, LET, AND CONST
Hoisting of declarations and functions
Predict what a name holds before its declaration line by reading each scope as bindings first, statements second.
What you will learn
- Read any scope in two passes: bindings are registered first, statements run second.
- Explain why reading a var before its line yields undefined, not a ReferenceError.
- Tell hoisted function declarations apart from functions stored in var, let, or const.
- Spot inner declarations that shadow an outer name from the top of their own scope.
Understanding Hoisting of declarations and functions
Before a single statement of a scope runs, the engine scans that scope and creates a binding for every name declared in it. The bindings do not all start out the same: a function declaration is created with its finished function object already attached, a var binding is created holding undefined, and a let, const, or class binding is created but marked as uninitialized so any read throws. Hoisting is the usual name for this setup pass, and the name is slightly misleading, because nothing in your source is actually relocated.
This explains the two behaviours people find surprising. Calling a function that is declared fifty lines lower works because the function object existed before the first statement executed. Reading a var before its line gives undefined because only the name was prepared during setup; the `= value` part is an ordinary assignment statement that still runs exactly where you wrote it. So `var count = 0` is two events at two different times: the binding at scope creation, the value when execution reaches that line.
The setup pass happens per scope, not once per file. Every call to a function creates a fresh scope and runs the pass again over that function body, and each block creates a scope for the let, const, and class declarations inside it. That is why a variable declared halfway down a function is already shadowing an outer name of the same identifier on the function's very first line. In practice the useful half is function declaration hoisting, which lets you put helpers below the code that uses them; the var undefined phase is something to recognise while debugging, not something to build on.
console.log(typeof greet);
console.log(greet("Ada"));
console.log(message);
var message = "set later";
console.log(message);
function greet(name) {
return "Hello, " + name;
}A declaration is registered when its scope is created, but only function declarations arrive with a value; every other binding gets its value when execution reaches the assignment.
Worked examples
Declaration versus function stored in a variable
Shows that only the function declaration form is usable before the line that defines it.
sayHi();
function sayHi() {
console.log("hi from a declaration");
}
try {
sayBye();
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
var sayBye = function () {
console.log("bye");
};Example explained
Line 1sayHi() on line 1 works because the setup pass built the whole function object before any statement ran.
Line 2The var sayBye binding also exists during the same pass, but it holds undefined, since its value comes from a later assignment.
Line 3Calling undefined is a TypeError, not a ReferenceError: the name was found, the value simply is not callable.
Line 4Moving the sayBye() call below its assignment is the only fix; the function expression cannot be hoisted with a value.
The pass runs per scope, not per file
Demonstrates that a var declared inside a function shadows an outer name from the function's first line.
var color = "blue";
function paint() {
console.log(color);
var color = "red";
console.log(color);
}
paint();
console.log(color);Example explained
Line 1Entering paint() creates a local color binding set to undefined, because paint declares color somewhere in its body.
Line 2The first log therefore reads the local binding, never the outer "blue" one, and prints undefined.
Line 3After the assignment on line 5 runs, the same local binding holds "red".
Line 4The outer color was never touched, so the last log still prints "blue".
let is registered too, just without a value
Proves that let bindings are set up on block entry by showing the outer name becomes unreachable.
let stage = "outer";
{
try {
console.log(stage);
} catch (err) {
console.log(err.constructor.name);
}
let stage = "inner";
console.log(stage);
}Example explained
Line 1If the inner let stage did not exist yet, the first log would have found the outer binding and printed "outer".
Line 2Instead it throws, which shows the block-scoped binding was created on block entry and shadows the outer one immediately.
Line 3The difference from var is the starting state: registered but uninitialized, so reading it throws rather than giving undefined.
Line 4Once line 9 runs the binding is initialized, and the second log prints "inner".
Important notes
Nothing is rewritten or relocated in your source; the engine simply creates the scope's bindings before executing its statements, and error line numbers still point at the original lines.
A function declaration written inside a block is block-scoped in strict mode and in modules, so hoisting makes it callable earlier in that block but not outside it.
Common mistakes
Assuming var hoists the value as well as the name, so an early read gives undefined and the arithmetic below silently produces NaN instead of an error.
Converting a hoisted function declaration into const fn = () => {} during a refactor, which breaks every call site above the definition with a ReferenceError.
Reading hoisting as "declarations move to the top of the file", then expecting an outer value inside a function that redeclares the same name with var.
Try it yourself
Change, predict, then run
In a browser editor, write a script that logs typeof for three names before their declarations: a function declaration, a var, and a const arrow function, wrapping each log in try/catch. Write down your prediction for all three lines before running it, then compare.
Open the JavaScript workspaceCheck your understanding
What does this print? function f() { console.log(typeof value); var value = function () {}; console.log(typeof value); } f();
- undefined, then function
- function, then function
- A ReferenceError on the first log
- function, then undefined
Show answer
Entering f() creates the local binding value holding undefined, so the first typeof reports undefined; the function object is only attached when the assignment on the next line runs, making the second log function. Answering "function, then function" assumes a function expression is hoisted with its value, but only the function declaration form does that; here the right-hand side is just a value assigned at runtime to a var.