Hoisting
Before executing any code, the JavaScript engine scans the current scope and registers all variable and function declarations in memory. This is hoisting — declarations behave as if they were moved to the top of their scope. What differs between var, let, const, and function is what gets registered and what value it starts with.
🎯 What you'll learn
- What Hoisting Actually Is
- Hoisting by Declaration Type
- Why
function sayByeAbove Throws a Different Error - Function Declarations vs Function Expressions
let/const/classand the TDZ
1What Hoisting Actually Is
Before executing any code, the JavaScript engine scans the current scope and registers all variable and function declarations in memory. This is hoisting — declarations behave as if they were moved to the top of their scope. What differs between var, let, const, and function is what gets registered and what value it starts with.
2Hoisting by Declaration Type
| Declaration | Hoisted? | Initial value | Usable before the line? |
|---|---|---|---|
var | ✅ | undefined | ✅ yes (reads as undefined) |
let | ✅ | none (TDZ) | ❌ ReferenceError |
const | ✅ | none (TDZ) | ❌ ReferenceError |
function declaration | ✅ | the full function | ✅ yes (fully callable) |
function expression / arrow assigned to let/const | only the variable | none (TDZ) | ❌ ReferenceError |
class declaration | ✅ | none (TDZ) | ❌ ReferenceError |
console.log(a); // undefined — var is hoisted and initialized
var a = 1;
console.log(b); // ReferenceError — TDZ
let b = 2;
greet(); // works! function declarations are fully hoisted
function greet() { console.log("hi"); }
sayBye(); // TypeError: sayBye is not a function
var sayBye = function () { console.log("bye"); };3Why function sayBye Above Throws a Different Error
var sayBye is hoisted and initialized to undefined. The assignment of the function expression happens later, at the original line. So calling sayBye() too early calls undefined(), which is a TypeError, not a ReferenceError.
4Function Declarations vs Function Expressions
// Declaration — hoisted whole, callable anywhere in scope
function declared() { return "I work everywhere in this scope"; }
// Expression — only the variable is hoisted, not the function body
const expressed = function () { return "I only work after this line"; };
// Arrow function — same rule as expressions
const arrow = () => "same as above";5let/const/class and the TDZ
These are hoisted (the engine knows they exist) but stay uninitialized until their declaration line executes. Referencing them earlier throws — this protects against relying on a variable before it has a real value, unlike var's silent undefined.
{
// TDZ for `x` starts here
console.log(typeof x); // ReferenceError — NOT "undefined"
let x = 5; // TDZ ends here
}Note: typeof on a truly undeclared variable returns "undefined" safely. typeof on a variable in its TDZ throws. This is a subtle but important distinction.
6Hoisting Inside Blocks
function declarations inside blocks (if, for, etc.) have inconsistent, engine-dependent hoisting behavior in non-strict mode. Always use function expressions or arrow functions inside blocks to avoid relying on this.
Under the HoodThe Two-Pass Compilation That Makes Hoisting Work▾
Hoisting isn't the engine "moving code around" — it's a direct consequence of how JavaScript is compiled in two distinct passes before your code actually runs.
Pass 1 — Creation Phase (binding instantiation)
Before executing a single statement, the engine scans the entire scope (function or block) and registers every declaration it finds — var, let, const, function, class — into that scope's Environment Record (V8's Context or stack-slot equivalent, see the Scope topic). This is a purely structural scan; no right-hand-side expressions are evaluated yet.
During this pass, each binding gets a slot, initialized differently depending on its kind:
var x; -> slot "x" created, initialized to undefined immediately
let y; -> slot "y" created, marked with the TDZ sentinel ("the hole")
function f(){...} -> slot "f" created AND fully initialized with the function object, right away
class C {} -> slot "C" created, marked with the TDZ sentinel, same as let/constThis single pass explains every hoisting rule from section 2 at once: var gets a real (if useless) default value immediately; function declarations get their entire value immediately (because the whole function body is already known at parse time — nothing needs to be "evaluated" to build a function object from a declaration); let/const/class get a slot but no usable value yet.
Pass 2 — Execution Phase
Only now does the engine run your code top to bottom, statement by statement. A var x = 5; line, at this point, is really just an assignment to the slot that Pass 1 already created — the "declaration" part already happened. A let y = 5; line does two things at once: it removes the TDZ sentinel from y's slot AND assigns the value.
Why this two-pass model produces the exact errors it does
console.log(typeof x); // "undefined" — slot exists (Pass 1), holds undefined, no error
var x = 5;
console.log(typeof y); // ReferenceError — slot exists (Pass 1), but holds the TDZ sentinel
let y = 5;typeof on a truly nonexistent binding returns "undefined" safely — but typeof on a binding that exists-but-is-in-its-TDZ hits the sentinel check and throws. The difference between "doesn't exist" and "exists but not yet initialized" is the entire hoisting mechanism in one sentence, and it's enforced by a real runtime check against that sentinel value, not a special-cased rule in the parser.
Function declarations inside blocks: why behavior varies
Historically (pre-ES6), engines didn't agree on how to hoist a function declared inside an if block — some hoisted it to the enclosing function scope (like var), others scoped it to the block. ES2015+ standardized "Annex B" semantics for non-strict-mode compatibility: the function is block-scoped for let-style lookups AND a var-style binding pointing at it is also created in the enclosing function scope, updated whenever the block executes. This dual-binding behavior is exactly why relying on it across engines/strict-mode settings is fragile — strict mode drops the Annex B var-style binding entirely.
⚠️ Common Pitfalls
- Assuming
let/constare "not hoisted" — they are hoisted, they're just uninitialized (TDZ). This distinction matters for interviews and for reasoning abouttypeof. - Calling a function-expression-based function before its assignment line, expecting declaration-style hoisting.
- Declaring
varinside anifblock and expecting it to disappear after the block — it doesn't; it's hoisted to the function top. - Relying on hoisted
functiondeclarations inside blocks across different engines/strict-mode settings.
📋 Quick Reference
var → hoisted, initialized to undefined
let/const → hoisted, uninitialized (TDZ) until declaration line
function → hoisted with full body, usable immediately
class → hoisted, uninitialized (TDZ), like let/const🎤 Interview Q&A
Q1What is hoisting?core▾
During the creation phase, declarations are registered before code runs: function declarations are fully hoisted (callable before their definition), var is hoisted and initialized to undefined, let/const/class are hoisted but uninitialized (temporal dead zone).
- Are function expressions hoisted? Only the variable, not the function value.
Q2What is the temporal dead zone?core▾
The period from the start of a block until a let/const/class declaration is evaluated; accessing the variable then throws a ReferenceError.
- Why does it exist? To catch use-before-declaration bugs.
Q3What does this log: console.log(a); var a = 1;?core▾
undefined, because the declaration is hoisted but the assignment isn't. With let, it throws a ReferenceError.
- And calling a function declared later? Works for function declarations.
Q4How do classes behave with hoisting?senior▾
Class declarations are hoisted but in the TDZ, so using a class before its declaration throws.
- Why the difference from functions? To avoid relying on partially defined classes.
Q5What's the practical advice about hoisting?core▾
Declare variables at the top of their scope, prefer const/let, and define functions before use for readability, even though declarations would hoist.
- Lint rule? no-use-before-define.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.