Hoisting

Understand what hoisting is, how var, let, const, and function declarations are hoisted differently, and what the temporal dead zone is and why it exists.

Hoisting is JavaScript's behavior of processing variable and function declarations before executing any code, during a setup phase for each scope. This explains some behavior that looks strange at first — like being able to call a function before the line where it's written — and some behavior that trips up beginners, like referencing a let variable too early and getting a strange error instead of undefined.

What hoisting actually is

Before JavaScript executes any code in a scope, it first scans that scope for variable and function declarations and registers them in memory. This is often described as "moving declarations to the top," but it's more accurate to say the JavaScript engine processes declarations in a preparation phase before running the code line by line.

The effect differs sharply depending on whether you used var, let, const, or a function declaration — this is where most of the confusion (and most interview questions) come from.

var hoisting

A variable declared with var is hoisted to the top of its enclosing function (or the global scope, if declared outside any function), and it is automatically initialized to undefined during the setup phase. This means you can reference a var variable before its declaration line without an error — you just get undefined instead of the value you expected.

Only the declaration is hoisted, never the assignment. console.log(x); var x = 5; does not throw, but it logs undefined, because at that point in execution only var x; has happened — the x = 5 assignment still runs later, in its original position.

Function declaration hoisting

Function declarations (function greet() { ... }) are hoisted completely — both the name and the entire function body are available before the line where they are written. This is why it is perfectly normal, and common, to call a function earlier in a file than where it is defined further down.

This does not apply to function expressions (const greet = function() { ... }) or arrow functions assigned to a variable — those follow the hoisting rules of whatever variable keyword (var, let, const) holds them, not the special full-hoisting rule of function declarations.

let and const hoisting, and the temporal dead zone

let and const declarations are technically hoisted too — the engine is aware of them from the top of the block — but unlike var, they are not initialized to undefined. Instead, they remain in an uninitialized state called the temporal dead zone (TDZ) from the start of the block until the line where they are actually declared.

Trying to access a let or const variable while it is in the temporal dead zone throws a ReferenceError: "Cannot access 'x' before initialization" — a much clearer error than var's silent undefined, and one of the main practical safety benefits of preferring let/const.

Comparing hoisting behavior

var: hoisted and initialized to undefined. let/const: hoisted but left uninitialized (temporal dead zone) until their declaration line runs. function declarations: hoisted with their full body, so they're callable before their written position.

console.log(a); // undefined (var hoisted + auto-initialized)
var a = 1;

try {
  console.log(b); // ReferenceError (TDZ)
} catch (e) {
  console.log(e.message);
}
let b = 2;

greet(); // works — function declarations fully hoisted
function greet() {
  console.log("hi");
}

Code Examples

The first console.log runs before the assignment line, but because var is hoisted (and initialized to undefined), it doesn't crash — it simply hasn't been assigned "Hello!" yet. Expected result: undefined Hello!

console.log(message); (first line) prints undefined because only the declaration, not the assignment, has been hoisted.

getDiscount is a function declaration, so its entire definition is hoisted to the top of processOrder's scope. It can be called before the line where it's textually written, without error. Expected result: 0.1 console.log(getDiscount()); calls a function that is defined later in the source but already fully available due to hoisting.

Even though reason is declared with let further down in the same block, the engine already knows the name exists (it's hoisted) — but it is in the temporal dead zone until its declaration line executes, so accessing it early throws instead of returning undefined. Expected result: Cannot access 'reason' before initialization console.log(reason); accesses reason while it is still in the temporal dead zone, before its let reason =... line has run.

Common mistake: Assuming let behaves exactly like var but 'a bit stricter' — in fact the TDZ makes let/const fail loudly and immediately, which is a feature, not a bug: it surfaces ordering mistakes as errors instead of silent undefined values.

console.log(message);
var message = "Hello!";
console.log(message);
function processOrder() {
  console.log(getDiscount());

  function getDiscount() {
    return 0.1;
  }
}

processOrder();
function checkStock(sku) {
  if (sku === "OUT-OF-STOCK") {
    console.log(reason); // TDZ! let is hoisted but not yet initialized
    let reason = "backordered";
  }
}

try {
  checkStock("OUT-OF-STOCK");
} catch (err) {
  console.log(err.message);
}

Hoisting phases inside a scope

Hoisting happens in a preparation pass before line-by-line execution begins; var, let/const, and function declarations are each treated differently during that pass.

SOURCE ORDER                 WHAT THE ENGINE DOES FIRST (setup phase)
─────────────                ──────────────────────────────────────
console.log(a)                var a  → registered, set to undefined
var a = 1                     let b  → registered, TDZ (no value yet)
console.log(b)                function greet(){} → registered, fully
let b = 2                                          usable immediately
function greet(){}

           │
           ▼
EXECUTION ORDER (line by line, top to bottom)
1. console.log(a)   → undefined  (a exists, no value assigned yet)
2. a = 1             → a becomes 1
3. console.log(b)   → ReferenceError (b still in TDZ)
4. b = 2             → b leaves the TDZ, becomes 2
5. greet() callable throughout, from line 1 onward

Important Rules

var declarations are hoisted and auto-initialized to undefined.

let and const declarations are hoisted but remain uninitialized until their declaration line runs — this window is the temporal dead zone.

Function declarations are hoisted completely, including their body, so they can be called before their written position in the code.

Function expressions and arrow functions assigned to let/const/var follow that variable's hoisting rules, not the function-declaration rule.

Always declare variables at the top of the scope you use them in — it avoids relying on hoisting quirks and makes code easier to read.

Where this shows up

Understanding hoisting explains why utility functions are often organized below the code that uses them (function declarations are hoisted), and why linters flag "used before defined" for let/const but not for var. It's also a very common interview topic, because it reveals whether a developer understands JavaScript's execution model rather than just its syntax.

if (isAdmin) { var permissions = "full"; } console.log(permissions); // "full" leaks out unexpectedly if (isAdmin) { let permissions = "full"; console.log(permissions); } // stays inside the block, as intended

Because var ignores block boundaries, the hoisted permissions variable is visible throughout the whole function, not just inside the if — which can silently leak values you meant to keep local.

[Multiple Choice] What happens when you access a let variable before its declaration line, inside the same block? A. It returns undefined, just like var B. It throws a ReferenceError because it is in the temporal dead zone C. It returns null D. JavaScript automatically moves the declaration up and runs it early

B. It throws a ReferenceError because it is in the temporal dead zone — let is hoisted but not initialized until its declaration line executes. Accessing it before that point throws a ReferenceError due to the temporal dead zone.

[True / False] A function expression such as const greet = function() {...} is hoisted the same way as a function declaration, so it can be called before its line in the code. A. True B. False

B. False — False. Only function declarations get full hoisting of their body. A function expression assigned to const follows const's hoisting rules (temporal dead zone) — calling it early throws an error.

Predict the console output of the following code, then explain why in a comment: console.log(typeof x); let x = 10; console.log(typeof y); var y = 20;

// console.log(typeof x) throws a ReferenceError, because x is // in the temporal dead zone (let is hoisted but not initialized). // The second console.log never runs, because the program has // already crashed. If x's line were removed, typeof y would // print "undefined", since var is hoisted and auto-initialized.

Think about which variable is in the temporal dead zone and which one is hoisted-and-initialized.