Before running a single line of code, JavaScript makes a pass over the scope and registers every variable and function declaration. It moves them — conceptually — to the top of the scope. That is hoisting.
It is not magic. It is a consequence of how the JavaScript engine compiles and then executes your code in two separate phases. The first phase registers; the second executes. Hoisting is the side effect of that split.
Functions: full hoisting
Function declarations are hoisted the most generously. They are fully available before the line where you wrote them.
saludar() // works — prints "Hola"
function saludar() {
console.log('Hola')
}
This is not an error. JavaScript already registered saludar and its complete implementation during the compilation phase. By the time it reaches the call, the function exists.
Arrow functions and functions assigned to variables do not behave the same way — they inherit the behavior of the variable they are assigned to.
var: hoisting with a catch
Variables declared with var are hoisted, but only the declaration — not the assignment. The engine knows the variable exists, but its value is undefined until execution reaches the assignment line.
console.log(nombre) // undefined — no error, but no value either
var nombre = 'Elias'
console.log(nombre) // "Elias"
What JavaScript runs internally looks roughly like this:
var nombre // hoisted — it exists, it holds undefined
console.log(nombre) // undefined
nombre = 'Elias'
console.log(nombre) // "Elias"
This behavior is the root of subtle bugs: the variable does not throw, so you assume it has a value — but it holds undefined. The error shows up later, somewhere else, and it is hard to trace.
let and const: the rules change
With let and const, hoisting still happens — the engine registers the declarations during the compilation phase. But there is a critical difference: you cannot use them before the line where they are declared.
console.log(nombre) // ReferenceError: Cannot access 'nombre' before initialization
let nombre = 'Elias'
The error is explicit. No silent undefined — the engine tells you exactly what happened.
The Temporal Dead Zone
The stretch between the start of the scope and the line where a let or const is initialized is called the Temporal Dead Zone (TDZ). During that stretch the variable exists in the scope — the engine already registered it — but you cannot access it.
{
// --- TDZ for 'usuario' starts here ---
console.log(usuario) // ReferenceError
// --- TDZ for 'usuario' ends here ---
let usuario = 'Elias'
console.log(usuario) // "Elias"
}
The TDZ is not an interval of time — it is an interval of code. How long it lasts depends on where you declared the variable inside the scope, not on how long execution takes.
Why the TDZ exists
The TDZ is not an accident — it is a deliberate design decision.
With var, the undefined value before assignment created a nasty class of bugs: code that ran without errors but with wrong data. The TDZ flips that logic around: if you touch a variable before initializing it, the error is immediate and explicit.
A ReferenceError at the exact site of the problem is infinitely more useful than an undefined that surfaces three functions later as an unexpected result.
The pattern that avoids all of this
The practical rule is simple: declare before you use. Always. And use let and const instead of var.
// Bad — you are relying on implicit hoisting
function calcular() {
console.log(total) // undefined, no error
var total = precio * cantidad
return total
}
// Good — declaration before use, explicit error if something breaks
function calcular() {
const total = precio * cantidad
console.log(total)
return total
}
const for values that do not change. let for values that do. var basically never — it gives you nothing let does not, and it adds confusing behavior on top.
One line per case
- Function declarations → fully hoisted, available across the whole scope
- var → the declaration is hoisted, not the assignment; value is
undefineduntil assignment - let / const → the declaration is hoisted, but the TDZ blocks access until initialization
Knowing about the TDZ does not change how you write JavaScript 99% of the time — if you declare before you use, you never hit it. What changes is that when a ReferenceError like this does show up, you know exactly what caused it.