Hoisting
Hoisting is JavaScript’s behavior of moving declarations to the top of their enclosing scope before any code runs. It is not literal code relocation — the engine simply registers declarations during the compilation phase, so identifiers exist (in some form) the moment a scope is entered. Understanding hoisting explains why a var can be read before its line, why a function call can appear above its definition, and why touching a let too early throws. Getting this wrong is one of the most common sources of subtle bugs.
How hoisting actually works
Before executing a scope, the engine scans it and creates bindings for every declaration it finds. What differs is when and how each binding is initialized:
vardeclarations are hoisted and immediately initialized toundefined.functiondeclarations are hoisted and fully initialized with their function body.let,const, andclassdeclarations are hoisted but left uninitialized — accessing them before their declaration line throws aReferenceError.
So everything is hoisted; only the initialization timing varies.
Variable hoisting with var
A var is usable before its declaration, but its value is undefined until the assignment line executes.
console.log(count); // undefined, not a ReferenceError
var count = 5;
console.log(count); // 5
Output:
undefined
5
The engine effectively treats the snippet as if the declaration were lifted:
var count; // hoisted, initialized to undefined
console.log(count); // undefined
count = 5; // assignment stays in place
console.log(count); // 5
Because var is function-scoped, this lifting happens to the top of the whole function, not the nearest block — a frequent surprise inside loops and if blocks.
Function hoisting
Function declarations are hoisted with their body, so you can call them before they appear in the source. This enables a top-down reading style where helpers live below their callers.
greet("Ada"); // works
function greet(name) {
return console.log(`Hello, ${name}`);
}
Output:
Hello, Ada
Function expressions and arrow functions are not hoisted as callable values. Only the variable binding is hoisted, following the rules of its var/let/const keyword.
sayHi(); // TypeError: sayHi is not a function
var sayHi = function () {
console.log("Hi");
};
Here sayHi is hoisted as a var (so it is undefined), and calling undefined throws a TypeError.
The temporal dead zone (TDZ)
let and const are hoisted too, but they sit in the temporal dead zone from the start of the scope until execution reaches their declaration. Reading or writing the binding in that window throws.
console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Grace";
The TDZ is a deliberate safety feature: it turns silent undefined bugs into loud, immediate errors. The variable exists during the TDZ — that is why typeof is not safe either:
console.log(typeof value); // ReferenceError (not "undefined")
let value = 10;
The TDZ is governed by runtime execution order, not line position. A
letreferenced inside a function is fine as long as the function runs after the declaration executes.
Comparing the declaration forms
| Declaration | Hoisted | Initial value before line | Scope | Access before declaration |
|---|---|---|---|---|
var | Yes | undefined | Function | Allowed, yields undefined |
let | Yes | none (TDZ) | Block | ReferenceError |
const | Yes | none (TDZ) | Block | ReferenceError |
function declaration | Yes | full function | Block/function | Callable |
function expression | Binding only | per var/let/const | per keyword | Not callable |
class | Yes | none (TDZ) | Block | ReferenceError |
A practical before/after
Relying on hoisting produces fragile code. The “before” version reads total while it is still undefined:
function cartTotal(items) {
applyTax(); // runs too early
var total = items.reduce((sum, i) => sum + i.price, 0);
function applyTax() {
console.log(total * 1.2); // NaN — total is undefined here
}
}
The “after” version declares before use and switches to const, so any ordering mistake surfaces as a clear error instead of NaN:
function cartTotal(items) {
const total = items.reduce((sum, i) => sum + i.price, 0);
const withTax = total * 1.2;
console.log(withTax);
return withTax;
}
Best practices
- Always declare variables before you use them; never depend on
varreading asundefined. - Prefer
constby default andletwhen reassignment is required — avoidvarentirely in modern code. - Let the TDZ work for you: it catches use-before-declaration bugs that
varwould hide. - Define function expressions and classes above the code that uses them, since they are not callable while hoisted.
- Keep declarations at the top of their block to make scope and lifetime obvious to readers.
- Do not rely on function-declaration hoisting to scatter helpers — group related logic for readability even though hoisting permits otherwise.