Skip to content
JavaScript js scope-closures 4 min read

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:

  • var declarations are hoisted and immediately initialized to undefined.
  • function declarations are hoisted and fully initialized with their function body.
  • let, const, and class declarations are hoisted but left uninitialized — accessing them before their declaration line throws a ReferenceError.

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 let referenced inside a function is fine as long as the function runs after the declaration executes.

Comparing the declaration forms

DeclarationHoistedInitial value before lineScopeAccess before declaration
varYesundefinedFunctionAllowed, yields undefined
letYesnone (TDZ)BlockReferenceError
constYesnone (TDZ)BlockReferenceError
function declarationYesfull functionBlock/functionCallable
function expressionBinding onlyper var/let/constper keywordNot callable
classYesnone (TDZ)BlockReferenceError

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 var reading as undefined.
  • Prefer const by default and let when reassignment is required — avoid var entirely in modern code.
  • Let the TDZ work for you: it catches use-before-declaration bugs that var would 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.
Last updated June 1, 2026
Was this helpful?