Many developers with three to five years of JavaScript experience give the exact same canned response when asked “What is hoisting?” during an interview: “Hoisting means variable declarations are moved to the top of their scope.”
That sounds fine on the surface, but probe a bit deeper:
- What is the difference between
var,let, andconstregarding hoisting? - Which gets hoisted higher: function declarations or function expressions?
- What exactly is the Temporal Dead Zone (TDZ)?
- Why is
aevaluated asundefinedinstead of10whenvar a = 10is hoisted?
At this point, answers often become vague and uncertain. Instead of memorizing interview cheat sheets, let’s start where JS execution actually begins—the compilation phase—to break down hoisting once and for all.
1. The Essence of Hoisting: “Registration,” Not “Relocation”
Many people assume hoisting means the browser physically shifts code to the top of the file. That is completely inaccurate.
The reality: Before executing code, the JS engine performs a “compilation scan” to complete the binding and registration of variables and functions. This process is called the Declaration Phase, whereas the actual execution is the Assignment Phase.
console.log(a);var a = 10;What actually happens?
- Compilation phase: The JS engine scans the code, encounters
var a, and registers variableain the environment record of the current scope, initializing it toundefined. - Execution phase: The engine reaches
console.log(a)and readsa, which is currentlyundefined. Continuing down toa = 10, it finally assigns10toa.
Thus, what appears to be “hoisting” is fundamentally: declarations are completed at compile time; assignments are completed at runtime.
2. var: The Most Misunderstood Hoisting Behavior
The True Face of var
console.log(a); // undefinedvar a = 10;This is equivalent to:
var a; // Compilation phase: declaration + initialized to undefinedconsole.log(a); // Execution phase: prints undefineda = 10; // Execution phase: assignmentThree critical points to remember:
- ✅ Declaration is hoisted
- ✅ Automatically initialized to
undefined - ❌ Assignment is not hoisted
This explains why you can access a variable that has been declared but not yet assigned without throwing a runtime error.
Function Scope of var
function test() { console.log(a); if (false) { var a = 10; }}test(); // undefinedEven though if (false) never executes, var a is still hoisted and belongs to the entire function scope.
3. Function Hoisting: Taking Precedence over Variables
Function Declarations: Complete Hoisting
foo();function foo() { console.log('hello');}✅ Executes normally. During the compilation phase, function declarations complete both declaration and assignment (pointing directly to the function body). This is equivalent to:
function foo() { console.log('hello');}foo();Function Expressions: Following var Rules
foo(); // TypeError: foo is not a functionvar foo = function () { console.log('hello');};Here, foo follows the exact same hoisting rules as var:
var foo; // Compilation phase: foo = undefinedfoo(); // Execution phase: undefined(), crashes immediatelyfoo = function () { ... }; // Assigned later⚠️ Pay attention to error types:
ReferenceError: The variable has not been declared at all.TypeError: The variable exists, but its type is incorrect (e.g., trying to invokeundefinedas a function).
Precedence: Function Declarations > Variable Declarations
console.log(foo);var foo = 'bar';function foo() {}Output: ƒ foo() {}
Reason: During the compilation phase, both function declarations and variable declarations are hoisted, but function declarations take higher priority and overwrite the placeholder slot for variables of the same name. However, if the variable is assigned a value during the execution phase, it will overwrite the reference back:
console.log(foo); // ƒ foo() {}var foo = 'bar';function foo() {}console.log(foo); // 'bar'4. let / const: Are They Really “Not Hoisted”?
This is where the single biggest misconception lies.
Enter the TDZ (Temporal Dead Zone)
console.log(a); // ReferenceErrorlet a = 10;From this, many jump to the conclusion that let is not hoisted. ❌ That conclusion is inaccurate.
In reality: let and const are hoisted, but they are not automatically initialized.
The ES6 specification introduced the Temporal Dead Zone (TDZ): spanning from the start of the scope up to the let / const declaration statement. During this window, although the variable already exists in the scope, it cannot be accessed, read, or written to.
{ // TDZ starts console.log(a); // ❌ Throws ReferenceError let a = 10; // TDZ ends}Core Differences: let vs var
| Feature | var | let / const |
|---|---|---|
| Is hoisted? | ✅ | ✅ |
| Auto-initialized? | ✅ (undefined) | ❌ |
| Scope | Function-scoped | Block-scoped |
| TDZ applies? | ❌ | ✅ |
Summary in one line: var is hoisted and initialized; let / const are hoisted but uninitialized.
The Special Constraint of const
const a; // SyntaxError: Missing initializer in const declarationconst not only lacks automatic initialization, but it must be assigned a value at declaration, otherwise a syntax error is thrown immediately.
5. Dissecting Classic Interview Questions Step-by-Step
Question 1: Scope Shadowing Traps
var a = 1;function foo() { console.log(a); var a = 2;}foo();Answer: undefined
Explanation: Inside foo, var a is declared. Within the function scope, a is hoisted and initialized to undefined, shadowing the outer global variable a. Therefore, undefined is logged.
Question 2: Mixed Hoisting & TDZ
console.log(typeof a);var a = 1;let b = 2;Answer:
'undefined' // typeof aReferenceError // referencing bExplanation: var a is hoisted and initialized to undefined, so typeof a safely returns 'undefined'. let b resides inside the TDZ, so referencing b (even using typeof b) immediately throws a ReferenceError. Recognizing typeof behavior inside the TDZ is a big bonus in technical interviews.
Question 3: var vs let in Loops
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0);}// Outputs: 3, 3, 3
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0);}// Outputs: 0, 1, 2Reason: var i — the entire loop shares a single function-scoped binding of i. let i — a new block-scoped binding for i is created for each iteration step. This is a practical demonstration of block scoping combined with hoisting rules.
6. Why Does Hoisting Exist? (Design Motivations)
Now that we understand what it is, let’s step back and look at why.
Supporting Mutual Function Invocation
function a() { b();}function b() { a();}Without hoisting, whichever function were placed first wouldn’t be able to find the other. Hoisting ensures all functions are registered during compilation, allowing mutual calls regardless of declaration order.
Historical Context
JavaScript was originally designed as a lightweight browser scripting language. To lower the barrier to entry, early language designers opted for automatic variable initialization and fault tolerance over strict error throwing. While reasonable at the time, these decisions left behind the quirks we navigate today.
7. Best Practices for Modern JS
Now that you understand hoisting, how should you structure your code in production?
-
Default to
let/const: Avoidvarentirely, enforce explicit block scopes, and prevent implicit bugs outside the TDZ. -
Declare before use: Even if
lettechnically hoists declarations behind the scenes, don’t rely on it:// Badconsole.log(a);let a = 10;// Goodlet a = 10;console.log(a); -
Prefer function expressions / arrow functions:
// Recommendedconst greet = () => {console.log('hi');};Compared to function declarations, arrow functions offer predictable hoisting, align with
constimmutability semantics, and integrate seamlessly with modules and tree-shaking.
8. Summary in One Sentence
Hoisting is not “code relocation”; it is “compile-time registration.”
var: Hoisted + auto-initialized → access allowed early, value isundefined.let/const: Hoisted, but uninitialized → access within the TDZ throws aReferenceError.- Function declarations: Fully hoisted, taking precedence over variable placeholders.
- Function expressions: Follow standard variable hoisting rules (
varorlet/const).
Once you can explain hoisting through the lens of Compilation → Execution → Scope → TDZ, you move past simple interview memorization to a true understanding of JavaScript execution mechanics.