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 all

Those 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 iteration

That 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 TDZ

Notice 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 const by default, and let when you need to reassign. Reach for var only when maintaining old code.
  • Declare variables before you use them. The TDZ only bites when you read something early, which is rarely intentional.
  • Prefer const for 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-define rule 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.

Frequently Asked Questions

Are let and const hoisted? Yes, both are registered at the top of their block, but they are left uninitialised. Accessing them before the declaration throws a ReferenceError rather than returning undefined. That gap is called the temporal dead zone.
What is the temporal dead zone? The region between entering a scope and reaching a let or const declaration, during which the variable exists but cannot be read. It exists to turn silent undefined bugs into loud errors at the point of the mistake.
Why does var give undefined instead of an error? Because var declarations are initialised to undefined during the creation phase, so the variable genuinely holds a value before your assignment runs. That silence is exactly why let and const were designed to throw instead.
Can I call a function before defining it? Yes for a function declaration, since the whole function is hoisted. No for a function expression assigned to a variable — with var you get a TypeError because it is still undefined, and with const a ReferenceError from the temporal dead zone.
Why does var in a loop print the same number three times? Because var is function-scoped, so all iterations share one variable, which has reached its final value by the time the asynchronous callbacks run. let creates a new binding per iteration, so each callback captures its own value.