How var, let, and const Differ in Hoisting and Scope: A Complete Guide from You-Dont-Know-JS
var is function-scoped, hoisted with immediate initialization to undefined, and allows re-declaration; let and const are block-scoped, hoisted but left uninitialized in the Temporal Dead Zone (TDZ) until their declaration line executes, and forbid re-declaration in the same scope.
The differences between var, let, and const in JavaScript extend far beyond simple syntax preferences. According to the authoritative source code analysis in the You-Dont-Know-JS repository by Kyle Simpson, these declaration keywords exhibit fundamentally distinct behaviors regarding hoisting and scope that directly impact how the JavaScript engine registers and initializes identifiers. Understanding these mechanics is essential for avoiding the subtle bugs caused by the Temporal Dead Zone and scope leakage.
Function Scope vs. Block Scope
The most fundamental distinction begins with where each declaration creates its binding.
var declarations are function-scoped. When declared inside a function, the identifier is accessible throughout the entire function body regardless of where the declaration appears. If declared outside any function, var creates a global property on the global object.
let and const declarations are block-scoped. They define identifiers that are only accessible within the nearest enclosing { … } block, whether that block belongs to an if statement, a for loop, or a standalone block. This prevents the "leakage" of loop counters and temporary variables into surrounding scopes.
As documented in scope-closures/ch2.md and scope-closures/ch6.md, this block-scoping behavior eliminates many of the accidental variable collisions that plagued pre-ES6 JavaScript codebases.
Hoisting Mechanics: How the Engine Registers Identifiers
Despite common misconceptions, all three declarations are hoisted—the JavaScript engine registers the identifier name at the top of its respective scope during the compilation phase. However, the critical difference lies in when and how the identifier is initialized.
var Hoisting and Auto-Initialization
When the engine encounters a var declaration, it hoists the identifier to the top of its enclosing function (or global) scope and immediately initializes it to undefined. This means the variable name exists and holds a value from the very beginning of the scope's execution.
console.log(greeting); // → undefined (name exists, value not yet set)
var greeting = 'Hello';
console.log(greeting); // → 'Hello'
As explained in scope-closures/ch5.md, this behavior creates the well-known "variable hoisting" effect where statements appearing before the var declaration can still reference the variable, albeit with an undefined value until the assignment executes.
let and const Hoisting with the Temporal Dead Zone
let and const declarations are also hoisted to the top of their enclosing block, but the engine does not initialize them immediately. Instead, the identifier remains in an uninitialized state until the execution reaches the actual declaration line.
This period between the start of the block and the declaration line is known as the Temporal Dead Zone (TDZ). During the TDZ, the identifier exists but cannot be accessed—any attempt to reference it throws a ReferenceError.
// console.log(message); // ❌ ReferenceError: Cannot access 'message' before initialization
let message = 'Hi';
console.log(message); // → 'Hi'
According to scope-closures/ch5.md, this design prevents the accidental use of uninitialized values while still allowing the engine to know the identifier exists for lexical analysis and scope resolution.
The Temporal Dead Zone (TDZ) Explained
The Temporal Dead Zone represents one of the most critical behavioral differences between var and the ES6 declarators. While var allows access (returning undefined) before its declaration, let and const enforce a strict "declaration-before-use" policy through the TDZ.
For const, the TDZ carries an additional constraint: the declaration must include an initializer. You cannot declare a const without assigning a value, as the syntax itself requires initialization.
const PI = 3.14159;
// const PI; // ❌ SyntaxError: Missing initializer
// PI = 3; // ❌ TypeError: Assignment to constant variable.
console.log(PI); // → 3.14159
As detailed in scope-closures/ch5.md, the TDZ ends precisely when the JavaScript engine evaluates the initializer expression during the declaration line's execution.
Re-declaration and Re-assignment Rules
Beyond hoisting and scoping, the three declarators enforce different rules regarding duplicate declarations and value assignment:
var: Allows re-declaration within the same scope (subsequentvardeclarations of the same name are effectively no-ops) and permits re-assignment at any time.let: Forbids re-declaration within the same block scope (throwsSyntaxError) but allows re-assignment after initialization.const: Forbids re-declaration and forbids re-assignment entirely (throwsTypeErroron assignment attempts). Note thatconstprevents rebinding of the identifier but does not make objects immutable.
Practical Code Examples from You-Dont-Know-JS
The following examples demonstrate the practical implications of these differences as documented in the repository's scope-closures chapters.
Block Scope Eliminates Variable Leakage
Unlike var, which would leak the loop counter into the surrounding function, let confines i and square to the for block:
for (let i = 0; i < 3; i++) {
const square = i * i;
console.log(i, square);
}
// console.log(i); // ❌ ReferenceError: i is not defined
// console.log(square); // ❌ ReferenceError: square is not defined
As shown in scope-closures/ch6.md, this block-scoping behavior prevents accidental variable collisions and makes closures in loops behave predictably.
Summary
vardeclarations are function-scoped, hoisted with immediate initialization toundefined, and permit re-declaration within the same scope.letandconstdeclarations are block-scoped, hoisted but not initialized (creating a Temporal Dead Zone until the declaration line), and forbid re-declaration within the same block.- The Temporal Dead Zone prevents access to
letandconstvariables before their declaration line executes, throwingReferenceErroron premature access. constrequires initialization at declaration and prevents re-assignment, whileletallows both initialization and later re-assignment.
Frequently Asked Questions
Are let and const hoisted like var?
Yes, let and const are technically hoisted—the JavaScript engine registers the identifier name at the top of the enclosing block during the compilation phase. However, unlike var, they are not initialized immediately. They remain in the Temporal Dead Zone until the execution reaches the declaration line, making them behave as if they are not hoisted in practice.
What is the Temporal Dead Zone in JavaScript?
The Temporal Dead Zone (TDZ) is the period between the start of a block and the execution of a let or const declaration line. During this zone, the identifier exists but cannot be accessed—any attempt to read or write the variable throws a ReferenceError. The TDZ ends precisely when the engine evaluates the initializer expression during the declaration's execution.
Can I redeclare a variable with let in the same scope?
No, attempting to declare a let variable with the same name in the same block scope throws a SyntaxError. This differs from var, which allows re-declaration within the same function scope (treating subsequent declarations as no-ops). The same prohibition applies to const—you cannot redeclare a const binding in the same scope.
Why does const prevent reassignment but not mutation?
const creates a read-only binding between the identifier and the value stored at the time of declaration. This prevents re-assignment of the identifier itself (the binding), but if the value is an object or array, the contents of that object can still be modified. To prevent mutation, you would need to use Object.freeze() or similar techniques in addition to const.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →