JavaScript Hoisting
Understand JavaScript hoisting — creation vs execution phase, var vs let vs const, function declarations vs expressions, and interactive memory visualizations.

Introduction
Hoisting is one of the most misunderstood ideas in JavaScript. Many developers picture the engine physically moving lines of code to the top of a file. That is not what happens.
Hoisting is the engine's behavior during the creation phase of an execution context: it scans declarations, allocates memory, and registers bindings before any statement runs. The source file order never changes — only the in-memory setup does.
This guide covers:
- The core concept — what hoisting really means
- Execution context phases — where hoisting happens
- Variable hoisting —
var,let, andconst - Function hoisting — declarations vs expressions
- Best practices — how to write code that avoids hoisting pitfalls
Related reading: JavaScript Execution Context for the full runtime model behind hoisting.
Course Reference
Quick index
| # | Section |
|---|---|
| 1 | What is hoisting? |
| 2 | Where hoisting happens |
| ↳ Hoisting memory scanner | |
| 3 | Variable hoisting |
| ↳ var vs let vs const demo | |
| 4 | Function hoisting |
| ↳ Function hoisting explorer | |
| 5 | Best practices |
1. What is hoisting?
When JavaScript enters a new execution context, the engine performs two distinct phases before your program finishes:
| Phase | What happens |
|---|---|
| Creation | Scan declarations, allocate memory, set up scope chain |
| Execution | Run code line by line, assign values, invoke functions |
Hoisting refers to the creation-phase behavior where the engine registers variable and function declarations in memory. It is a conceptual label — no code is physically moved to the top of the file.
console.log(greeting) // undefined (not ReferenceError)
var greeting = 'Hello'
sayHi() // 'Hi!' — works before the function line
function sayHi() {
console.log('Hi!')
}From reading the file top to bottom, greeting and sayHi appear to be used before they exist. In reality, the creation phase already registered both before line 1 executed.
2. Where hoisting happens
Hoisting is not a standalone feature — it is a consequence of how execution contexts boot up.
Creation phase checklist
- Create the variable environment and lexical environment
- Scan the scope for
var,function,let, andconstdeclarations - Allocate memory for each binding
- Initialize
vartoundefinedand store full function declarations - Leave
let/constuninitialized (temporal dead zone) - Set up the outer scope reference and
thisbinding
Execution phase checklist
- Run statements top to bottom
- Assign actual values to variables
- Execute function bodies when functions are invoked
// Creation phase registers: count → undefined, greet → full function
// Execution phase runs each line in order
console.log(typeof greet) // 'function'
console.log(typeof count) // 'undefined'
greet()
var count = 10
function greet() {
return 'Hello'
}
let label = 'JS'Interactive: hoisting memory scanner
Hoisting Memory Scanner
See what the engine registers in the creation phase before any line runs — no code physically moves to the top.
hoisting.js
Engine scans declarations and allocates memory — zero lines executed
undefined
ƒ greet() { return "Hello" }
⟨uninitialized⟩
Temporal Dead Zone
3. Variable hoisting
Not all declarations behave the same during creation.
| Declaration | Hoisted? | Initialized in creation? | Access before declaration line |
|---|---|---|---|
var | Yes | undefined | Returns undefined — no error |
let | Yes | No (TDZ) | ReferenceError |
const | Yes | No (TDZ) | ReferenceError |
var — initialized to undefined
console.log(x) // undefined
var x = 5
console.log(x) // 5The binding exists before line 2 runs, but its value is undefined until assignment.
let and const — temporal dead zone
console.log(y) // ReferenceError
let y = 5
console.log(z) // ReferenceError
const z = 10Both are registered in memory during creation, but accessing them before their declaration line throws a ReferenceError. This is the temporal dead zone (TDZ).
Interactive: variable hoisting compare
Variable Hoisting Compare
Toggle var vs let vs const and see what happens when you access a variable before its declaration line.
Memory
x → undefined
Console result
undefined (no error)
All three are hoisted into memory during creation. Only var is initialized to undefined. let and const stay in the temporal dead zone until their declaration line executes.
4. Function hoisting
Functions follow different rules depending on how they are defined.
Function declarations — fully hoisted
greet() // 'Hello'
function greet() {
return 'Hello'
}The entire function — name and body — is stored in memory during creation. You can call it before the declaration line.
Function expressions — variable hoisting rules apply
sayHi() // TypeError: sayHi is not a function
var sayHi = function () {
return 'Hello'
}Here sayHi is a var. Creation phase sets it to undefined. Calling it before assignment throws TypeError because undefined is not callable.
sayHi() // ReferenceError
const sayHi = () => 'Hello'With const or let, the binding is in the TDZ — you get ReferenceError, not TypeError.
Named function expressions
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n - 1)
}
// `fact` is only visible inside the function bodyThe outer name follows variable hoisting rules; the inner fact name is local to the function.
Interactive: function hoisting explorer
Function Hoisting Explorer
Compare function declarations vs expressions — see what is stored in memory and what happens when you call before the definition line.
Creation phase memory
sayHi → ƒ sayHi() { return "Hello" } (full body stored)
sayHi() on line 1
'Hello'
Function declarations are fully hoisted. The entire function — name and body — is available before the declaration line runs.
| Type | Creation phase | Call before line |
|---|---|---|
| Declaration | Full function stored | Works |
| Expr + var | Variable → undefined | TypeError |
| Expr + const/let | TDZ | ReferenceError |
5. Best practices
Hoisting explains odd behavior — it should not be something you depend on in production code.
Declare before use
// Avoid
console.log(total)
var total = calculateTotal()
// Prefer
function calculateTotal() {
return 100
}
const total = calculateTotal()
console.log(total)Use let and const instead of var
var hoisting to undefined silently hides bugs. Block-scoped let/const with TDZ catch mistakes at the declaration line.
// var leaks and hoists oddly in blocks
for (var i = 0; i < 3; i++) {}
console.log(i) // 3
// let stays block-scoped
for (let j = 0; j < 3; j++) {}
// console.log(j) // ReferenceErrorPut function declarations before calls (or use expressions after setup)
// Clear intent — no reliance on hoisting
function init() {
bindEvents()
fetchData()
}
function bindEvents() {
/* ... */
}
function fetchData() {
/* ... */
}
init()Understand execution context for debugging
When you see unexpected undefined, TDZ errors, or "is not a function", trace:
- Which execution context is active?
- Has the creation phase registered this binding?
- Has the declaration line executed yet?
Conclusion
Hoisting is not magic and it is not code rearrangement. It is the engine preparing memory during the creation phase of each execution context.
Key takeaways:
- Hoisting = creation-phase memory allocation for declarations
var→ hoisted and initialized toundefinedlet/const→ hoisted but TDZ until the declaration line- Function declarations → fully hoisted with body
- Function expressions → follow the variable rules of their binding
- Best practice → declare before use; prefer
let/constovervar
Pair this guide with JavaScript Execution Context for the full picture of how code actually runs.
Have questions about hoisting or the temporal dead zone? Reach out via the contact page.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime