JavaScript declarations guide
JavaScript Hoisting: Declarations, Initialization, and the Temporal Dead Zone
A practical explanation of why var, let, const, and function declarations behave differently before their source lines run.
Hoisting is shorthand for several different language rules. Treating it as “JavaScript moves code to the top” creates more confusion than it solves.
A better model separates preparation of a scope from evaluation of statements. Different declaration forms create bindings and initialize values at different points in that process.
The short answer
- var creates a binding initialized to undefined before its assignment statement is evaluated.
- let and const bindings exist before their declaration is evaluated, but accessing them too early throws because they are uninitialized.
- Function declarations are initialized during the surrounding code’s instantiation, unlike a function expression assigned to a variable.
1. var is available before its assignment
The name message exists when the first console.log runs, but its assignment has not yet been evaluated. The observed value is therefore undefined, not the later string value.
This behavior is often called var hoisting. The useful detail is the initialized undefined value, which is different from a missing or uninitialized lexical binding.
message is initialized to undefined before the assignment runs
console.log(message); // undefined
var message = "ready";
console.log(message); // "ready"2. let and const have an uninitialized period
A lexical declaration creates a binding for its scope, but JavaScript does not permit reading that binding before the declaration has been evaluated. This period is commonly called the temporal dead zone.
The result is a ReferenceError, not undefined. That difference prevents a class of mistakes where an uninitialized value might otherwise look intentional.
Accessing status before its declaration throws
console.log(status); // ReferenceError
let status = "ready";3. Function declarations and expressions are not interchangeable here
The declaration named announce is initialized before the call expression is evaluated. By contrast, run is a var binding whose value is undefined until its assignment receives the function expression.
Prefer the form that makes the surrounding code clear. The point is not that one syntax is always better, but that their initialization rules are different.
The declaration works early; the var function expression does not
announce();
function announce() {
console.log("declared");
}
run(); // TypeError: run is not a function
var run = function () {
console.log("expressed");
};4. Predict the trace instead of memorizing a slogan
For each name, ask two separate questions: when is its binding created, and when does it receive its usable value? This predicts the behavior of common examples without pretending that source lines physically move.
Use the How JS Runs visualizer with a small program containing one declaration at a time. Step through preparation and evaluation to compare the visible binding state with the eventual output.