What you'll learn
Quick Answer
Hoisting means declarations are registered before any code runs, so the engine knows a name exists before the line that declares it. var declarations are initialised to undefined immediately, which is why reading one early gives undefined instead of an error. let and const are registered but left uninitialised, so reading them early throws a ReferenceError — the temporal dead zone. Function declarations are fully hoisted and callable before their definition; function expressions are not.
What Hoisting Really Is
The common explanation — "declarations move to the top" — is a useful picture but not what happens, and it fails to explain the errors you actually see.
JavaScript runs code in two phases. In the creation phase, the engine scans the scope and registers every declaration it finds. Only then does the execution phase run your statements line by line.
So nothing moves. The engine simply already knows which names exist in this scope before executing anything. What differs between var, let, const and functions is what value they hold during that gap.
console.log(a); // undefined — declared and initialised to undefined
var a = 5;
console.log(b); // ReferenceError — declared, but not initialised
let b = 5;
console.log(c); // ReferenceError: c is not defined — never declared at allThose are three genuinely different states, and telling them apart is what the topic is really about.
var: Hoisted and Initialised to undefined
A var declaration is registered and given the value undefined during the creation phase. The assignment stays where you wrote it.
function demo() {
console.log(count); // undefined, not an error
var count = 10;
console.log(count); // 10
}This is why var bugs are quiet. You get undefined rather than a crash, so the failure surfaces later and somewhere else — often as "cannot read property of undefined".
var is also function-scoped, not block-scoped, which produces the classic loop surprise:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 3, 3, 3 — one shared i, already 3 by the time the callbacks run
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 0, 1, 2 — let creates a fresh binding each iterationThat single difference is the most common reason to prefer let, and it is asked in interviews constantly.
let and const: The Temporal Dead Zone
let and const are hoisted too — this is the part people get wrong. They are registered in the scope, but deliberately left uninitialised.
The stretch between entering the scope and reaching the declaration is the temporal dead zone. Touching the variable there throws.
{
// TDZ for value starts here
console.log(value); // ReferenceError: Cannot access 'value' before initialization
let value = 42; // TDZ ends here
console.log(value); // 42
}Read the error message carefully — cannot access before initialization is different from is not defined. The first means the engine knows the variable exists; the second means it does not exist at all. That distinction tells you immediately whether you have a TDZ problem or a typo.
Proof that they really are hoisted: a let in an inner scope shadows an outer variable across the entire block, including before its declaration line.
let x = 'outer';
{
console.log(x); // ReferenceError, NOT 'outer'
let x = 'inner'; // this declaration governs the whole block
}If let were not hoisted, that first log would print 'outer'. It throws, which shows the inner declaration was registered from the top of the block.
Function Declarations vs Expressions
Function declarations are fully hoisted — name and body together — so they can be called before they appear in the file.
greet(); // works
function greet() { console.log('hi'); }Function expressions are not. Only the variable is hoisted, following its own rules.
greet(); // TypeError: greet is not a function
var greet = function () {}; // var is undefined at call time
hello(); // ReferenceError: Cannot access 'hello'
const hello = () => {}; // const is in the TDZNotice the two errors differ. With var the name exists but holds undefined, so calling it is a TypeError — you tried to invoke a non-function. With const the name is unreachable, so it is a ReferenceError.
Being able to explain that difference is a strong signal in an interview, because it shows you understand the mechanism rather than a rule of thumb.
One caution: a function declaration inside an if block behaves inconsistently across environments. If you need a conditional function, assign a function expression to a let or const instead.
What to Actually Do
The practical advice is short, and it makes most hoisting questions irrelevant in your own code.
- Use
constby default, andletwhen you need to reassign. Reach forvaronly when maintaining old code. - Declare variables before you use them. The TDZ only bites when you read something early, which is rarely intentional.
- Prefer
constfor functions assigned to variables. You lose call-before-definition, which is a mild loss and arguably a readability gain. - Let the linter help. The
no-use-before-definerule catches these at edit time rather than runtime.
Two things worth remembering regardless. const prevents reassignment, not mutation — you can still push to a const array or edit a const object's properties, because only the binding is fixed. And in an ES module, top-level declarations are scoped to the module rather than becoming global properties, which is another reason modern code sees fewer of these problems.
