Hoisting
Reviewed & published by Brayan K
Ever called a function above where you wrote it and wondered why it still worked? Or logged a var before assigning it and gotten undefined instead of an error? That's hoisting — JavaScript's two-phase way of preparing each scope before running it.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Understanding it demystifies a whole category of "how is this even running?" moments and makes you far better at reading other people's code.
What You'll Learn in This Lesson
- The two phases: creation and execution
- Why function declarations run early
- How var hoists as undefined
- Why function expressions don't hoist
- let/const and the Temporal Dead Zone
- Writing hoisting-safe code
📚 Prerequisites: Know your variables and functions. The next lesson on Scope pairs perfectly with this one.
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
🎬 Real-World Analogy: Think of a stage crew before a play:
- • Before the show (creation phase): the crew sets out every named prop slot on the stage
- • Function declarations are fully built props, ready to use from the start
- • var slots are placed but empty (undefined) until an actor fills them
- • let/const slots exist but are roped off (the TDZ) until their cue arrives
1️⃣ The Two-Phase Model
JavaScript runs each scope in two passes. First the creation phase scans the code and registers every declared name. Then the execution phase runs your code top to bottom, doing the actual assignments. "Hoisting" is just the name for that first pass making names available before the lines that assign them.
// We call greet() BEFORE its declaration appears — and it works!
console.log(greet("Ada")); // ✅ Expected output: Hello, Ada
function greet(name) {
return "Hello, " + name;
}
// The creation phase registered greet fully before execution began.2️⃣ var Hoists as undefined
A var declaration is hoisted, but only the name — the value isn't assigned until its line runs. So reading it earlier gives undefined rather than an error. This silent undefined is exactly the kind of bug let/const were designed to prevent.
console.log(score); // ✅ Expected output: undefined (declared, not yet assigned)
var score = 50;
console.log(score); // ✅ Expected output: 50
// The engine effectively treats it like:
// var score; // hoisted to the top, value undefined
// console.log(score); // undefined
// score = 50; // assignment happens here
// console.log(score); // 503️⃣ Function Expressions Don't Get the Full Treatment
Only function declarations are fully hoisted. A function stored in a variable (a function expression or arrow) follows the variable's rules. With var the name is hoisted as undefined, so calling it early throws "is not a function".
// Declaration — fully hoisted, callable early:
console.log(double(4)); // ✅ Expected output: 8
function double(n) { return n * 2; }
// Expression — only the var name is hoisted (as undefined):
// console.log(triple(4)); // ❌ TypeError: triple is not a function
var triple = function (n) { return n * 3; };
console.log(triple(4)); // ✅ Expected output: 12 (works AFTER assignment)🎯 Your Turn #1 — Predict var
Fill in the expected value the first log prints, given how var hoists.
// 👉 Before running: what will the FIRST log print? Write it after the ___
console.log(city); // your guess: ___
var city = "Oslo";
console.log(city); // Oslo
// Run it and compare. (Hint: var is hoisted but not yet assigned.)4️⃣ let / const and the Temporal Dead Zone
let and const are hoisted too — but into the Temporal Dead Zone. The name exists from the top of the block, yet touching it before its declaration throws a ReferenceError. That's a feature: it converts use-before-declare mistakes from silent undefined bugs into immediate, obvious errors.
// We GUARD the early access so the demo runs and shows the difference:
try {
console.log(token); // touching a let before its line...
let token = "abc";
} catch (e) {
console.log(e.constructor.name); // ✅ Expected output: ReferenceError
}
// Used correctly (after declaration) it's perfectly normal:
let token = "abc";
console.log(token); // ✅ Expected output: abc5️⃣ Putting It Together
The behaviour you see depends entirely on how the name was declared. Here's the whole story in one runnable snippet, with guards so nothing crashes the demo.
// 1) function declaration — callable early
console.log(typeof sayHi); // ✅ Expected output: function
// 2) var — hoisted as undefined
console.log(typeof a); // ✅ Expected output: undefined
// 3) let — in the TDZ (guarded so we don't crash)
try { b; } catch (e) { console.log(e.constructor.name); } // ✅ Expected output: ReferenceError
function sayHi() { return "hi"; }
var a = 1;
let b = 2;🎯 Your Turn #2 — Make It Safe
Reorder the snippet (move the declaration up) so it logs 9 without relying on hoisting.
// 👉 Move the 'const area' line ABOVE the log, then run.
console.log(area); // should print 9 once you reorder
const area = 3 * 3;🧩 Reorder Challenge
These lines are scrambled. Reorder them so the code runs cleanly with no TDZ error and logs 10.
Why: the function declaration may sit anywhere (it's fully hoisted), but price is a const — it must be declared before any line that reads it.
🧠 Quick Recall
Predict the output before revealing the answer.
undefined — var x is hoisted, but the value 7 isn't assigned until its line runs.
ReferenceError — y is in the Temporal Dead Zone until its declaration line.
hi — function declarations are fully hoisted, so calling before the line is fine.
Common Errors & Fixes
- 1️⃣ Relying on var's undefined
- 2️⃣ Calling a function expression too early
- 3️⃣ Assuming hoisting reorders assignments
- 4️⃣ Re-declaring with let in the same scope
Quick Reference — Hoisting
| Declared with | Hoisted? | Access before line |
|---|---|---|
| function declaration | ✅ Fully | Works — callable |
| var | ✅ Name only | undefined |
| let | ✅ Into TDZ | ReferenceError |
| const | ✅ Into TDZ | ReferenceError |
| function expression | Follows its var/let | undefined / TDZ |
Lesson Complete — Hoisting!
- ✅ Each scope runs in two phases: creation then execution
- ✅ Function declarations are fully hoisted; var hoists as undefined
- ✅ let/const hoist into the TDZ — a loud ReferenceError
- ✅ Declare before use; prefer let/const over var
Practice quiz
What are the two phases JavaScript runs each scope in?
- Parse and compile
- Creation and execution
- Read and write
- Hoist and assign
Answer: Creation and execution. First the creation phase registers declared names, then the execution phase runs the code and does assignments.
What does console.log(score) print if it runs before var score = 50?
- 50
- ReferenceError
- undefined
- null
Answer: undefined. var hoists the name but not the value, so reading it before assignment gives undefined.
Can you call a function declaration before the line where it is written?
- Yes, function declarations are fully hoisted
- No, it always throws
- Only if it uses var
- Only in strict mode
Answer: Yes, function declarations are fully hoisted. Function declarations are fully hoisted, so calling greet() before its declaration works.
What happens when you access a let variable before its declaration line?
- It returns undefined
- It returns null
- It throws a ReferenceError (Temporal Dead Zone)
- It returns 0
Answer: It throws a ReferenceError (Temporal Dead Zone). let and const are hoisted into the Temporal Dead Zone, so early access throws a ReferenceError.
Are function expressions assigned to a variable fully hoisted like declarations?
- Yes, identically
- No, they follow the variable's hoisting rules
- Yes, but only with const
- No, they are never hoisted at all
Answer: No, they follow the variable's hoisting rules. Only function declarations are fully hoisted; an expression follows its variable's hoisting (undefined for var, TDZ for let/const).
After hoisting, does the value assignment also move to the top of the scope?
- Yes, the whole line moves up
- No, only the declaration is hoisted; the assignment stays put
- Yes, but only for var
- No, nothing is hoisted
Answer: No, only the declaration is hoisted; the assignment stays put. Hoisting moves only the declaration; the assignment happens where it is written, which is why var reads undefined first.
What does typeof on a var name print before its assignment line runs?
- 'number'
- 'undefined'
- 'function'
- It throws
Answer: 'undefined'. A hoisted var is undefined until assigned, so typeof returns the string 'undefined'.
Why were let and const designed to use the Temporal Dead Zone?
- To run faster
- To turn use-before-declare bugs into loud errors instead of silent undefined
- To allow re-declaration
- To make them global
Answer: To turn use-before-declare bugs into loud errors instead of silent undefined. The TDZ converts sneaky use-before-declare mistakes into immediate, obvious ReferenceErrors.
What error occurs from let p = 1; let p = 2; in the same scope?
- TypeError
- ReferenceError
- SyntaxError: already declared
- No error
Answer: SyntaxError: already declared. Re-declaring the same name with let in one scope is a SyntaxError; reassign with p = 2 instead.
Is hoisting literally moving your source code to the top of the file?
- Yes, the engine rewrites the file
- No, it is a mental model for the two-phase creation-then-execution process
- Yes, but only function declarations move
- No, hoisting does not exist
Answer: No, it is a mental model for the two-phase creation-then-execution process. Source stays put; hoisting just describes how the creation phase registers names before execution, behaving AS IF they moved up.
Continue this course
- Previous: Type Coercion (== vs ===)
- Next: Scope & var/let/const (TDZ) — Block vs function scope, the temporal dead zone, and choosing let vs const
- Quick reference: JavaScript cheat sheet
Frequently asked questions
What exactly gets hoisted?
Declarations are hoisted, not assignments. The engine scans each scope before running it and registers the names it finds. Function declarations are hoisted fully (you can call them before they appear). var variables are hoisted but start as undefined. let and const are hoisted too, but stay unusable until their line — the Temporal Dead Zone.
If let is hoisted, why do I get a ReferenceError?
Because let and const are hoisted into the Temporal Dead Zone (TDZ). The name exists from the top of the block, but the engine refuses to let you read or write it until execution reaches the declaration. That deliberate error catches use-before-declare bugs that var would have hidden behind undefined.
Are function expressions hoisted like function declarations?
No. Only function DECLARATIONS (function foo() {}) are fully hoisted. A function expression assigned to a variable (const foo = function () {} or an arrow) follows the variable's rules: the name is hoisted, but the function value isn't assigned until that line runs. Call it earlier and you get the variable's hoisting behaviour, not a callable function.
Should I rely on hoisting in real code?
Generally no. Calling a function above its declaration is acceptable and common, but reading variables before they're declared is a code smell. Modern style declares things before use and prefers let/const, whose TDZ turns sneaky bugs into loud errors. Understanding hoisting is mostly about reading other people's code and debugging.
Is hoisting actually moving my code to the top of the file?
Not literally. Your source stays where it is. Hoisting is just a mental model for a two-phase process: first the engine creates the bindings for a scope (the 'memory setup' phase), then it runs the code line by line (the 'execution' phase). It behaves AS IF declarations were moved up.