Hoisting
Hoisting is the name for JavaScript's habit of acting as if certain declarations were moved to the top of their scope. It is not a literal rewrite of your code — it is a side-effect of how the engine sets up a scope before running it. Understanding hoisting clears up a lot of "why does this run but throw undefined?" confusion.
The mental model: two passes
Before executing a block, the engine does a quick first pass to find every declaration:
Function declarations are fully registered — both the name and the function body — before any code runs.
vardeclarations are registered with the valueundefined. The assignment itself stays where you wrote it.letandconstdeclarations are registered but uninitialised — reading them before the line where they are declared throws (this is the Temporal Dead Zone).
Function declarations — fully hoisted
hello(); // works fine — prints "hi"
function hello() {
console.log("hi");
}Both the name hello and its body are available from the start of the scope. This is what makes the classic "list helper functions below the main code" style legal.
`var` — name hoisted, value isn't
console.log(x); // undefined — not a ReferenceError var x = 5; console.log(x); // 5
The first line works because var x is registered at the top of the scope with the value undefined. The = 5 part runs on the third line. This is almost never what you want, and is one reason modern code prefers let/const.
What the engine effectively sees
var x; // hoisted to the top, value: undefined console.log(x); // undefined x = 5; // assignment stays here console.log(x); // 5
Function expressions are not function declarations
Only the function name() {} statement form is hoisted with its body. A function expression assigned to a variable follows the rules of that variable.
// hi(); // TypeError — hi is undefined right now
var hi = function () { console.log("hi"); };
hi(); // works after the assignment// bye(); // ReferenceError — TDZ
const bye = function () { console.log("bye"); };
bye();`let` and `const` — the Temporal Dead Zone
let and const are hoisted in the sense that the engine knows about them, but they remain uninitialised until the line that declares them runs. Any access before that line throws. This window is called the Temporal Dead Zone (TDZ).
console.log(name); // ReferenceError: Cannot access 'name' before initialization let name = "Ada";
The TDZ exists on purpose — it catches bugs where you accidentally read a variable above where you meant to declare it.
Why hoisting feels surprising
The most common confusion comes from mixing function declarations and expressions inside the same file. The two look almost identical but behave very differently when used before their definition.
run();
function run() { console.log("a"); } // ok — declaration
// crash();
// function crash() { ... } overwritten later? Let's see:
function crash() { console.log("first"); }
function crash() { console.log("second"); } // wins — later declaration overrides
crash(); // "second"Practical rules
Use
letandconsteverywhere. The TDZ turns a silent bug into a clear error.Declare variables before you use them, even though function declarations technically allow the opposite. It reads better.
If you depend on hoisting to make a file readable (helper functions below main code), that is fine — function declarations are designed to support that.
Be wary of
varin legacy code: it does not respect block scope (only function scope) and is hoisted withundefined.