JS let vs const: An Advanced Memory Deep Dive

An advanced exploration of JavaScript let vs const from a memory and engine perspective. Goes beyond syntax to cover temporal dead zone implementation, block scope mechanics in the V8 engine, const immutability vs value mutability, and why const is preferred for performance hints.

JavaScriptintermediate
11 min read

Most tutorials cover let and const at the surface level: let allows reassignment, const does not. This article goes deeper — into how the temporal dead zone actually works, what const really guarantees (and what it does not), how engines can use const as an optimization hint, and practical guidelines for choosing between them.

What let and const Actually Guarantee

javascriptjavascript
let x = 1;
x = 2; // ✓ reassignment allowed
 
const y = 1;
y = 2; // ❌ TypeError: Assignment to constant variable.
 
// const with objects — the BINDING is constant, not the value
const obj = { a: 1 };
obj.a = 99;   // ✓ modifying the object — allowed
obj.b = 100;  // ✓ adding properties — allowed
obj = {};     // ❌ reassigning the variable — TypeError
 
const arr = [1, 2, 3];
arr.push(4);  // ✓ mutating the array — allowed
arr = [];     // ❌ reassigning — TypeError

const means: this variable binding will not be reassigned. It says nothing about whether the value it points to can be mutated.

The Temporal Dead Zone (TDZ) — What Actually Happens

Both let and const are hoisted to the top of their block scope but are NOT initialized until the declaration line is reached. The gap between hoisting and initialization is the Temporal Dead Zone:

javascriptjavascript
console.log(x); // ❌ ReferenceError: Cannot access 'x' before initialization
let x = 5;
console.log(x); // 5
 
// This differs from var:
console.log(v); // undefined — var is hoisted AND initialized to undefined
var v = 5;

What the TDZ Looks Like in Engine Implementation

The JavaScript engine:

  1. Scans the block scope for all let/const declarations
  2. Allocates storage for them but marks them as "uninitialized"
  3. Any read or write to an uninitialized binding throws a ReferenceError
  4. When execution reaches the declaration line, the binding transitions to "initialized"
javascriptjavascript
{
  // TDZ for message starts here (hoisted but uninitialized)
  console.log(message); // ❌ ReferenceError — in TDZ
  const message = "Hello"; // TDZ ends here — now initialized
  console.log(message); // "Hello" ✓
}
// message does not exist here — block scoped

The TDZ exists for let and const, not var. This is why you get a ReferenceError (not undefined) for pre-initialization access — it is a signal that the code has a likely bug.

Block Scope vs Function Scope

var is function-scoped; let and const are block-scoped (any {}):

javascriptjavascript
function scopeDemo() {
  if (true) {
    var  funcScoped = "visible outside if";
    let  blockScoped = "only inside if";
    const alsoBlock = "only inside if";
  }
  console.log(funcScoped);  // "visible outside if" ✓
  console.log(blockScoped); // ❌ ReferenceError
}
 
// Block scope in loops — critical for closures
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3 — shared var i
}
 
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 0, 1, 2 — new let j per iteration
}

The let loop behavior is often cited as the most practically important difference from var. Each loop iteration creates a new binding for let — meaning closures capture different variables.

Memory Implications: const as an Engine Hint

When you declare a variable with const, you signal to the JavaScript engine that the binding will not change. Modern engines like V8 can use this information for optimizations:

  1. No need to check for rebinding: The engine can skip the reassignment check at runtime
  2. Inline caching benefits: If a const holds an object, the engine may be more confident about the object's shape not changing (combined with object shape stability rules)
  3. Dead code elimination: If the engine knows a const is a specific primitive at compile time, it can sometimes inline the value
javascriptjavascript
// Engine potentially more confident about this:
const PI = 3.14159;
function circleArea(r) { return PI * r * r; }
 
// Than this (must re-check PI may have been reassigned):
let pi = 3.14159;
function circleArea2(r) { return pi * r * r; }

Modern engines are sophisticated enough that this difference is often negligible in practice — but it is the intent signaling that matters for code readability and maintainability.

Object.freeze — Actual Immutability

To make the value immutable (not just the binding), use Object.freeze():

javascriptjavascript
const config = Object.freeze({
  apiUrl: "https://api.example.com",
  timeout: 5000,
  maxRetries: 3,
});
 
config.timeout = 9999; // Silently fails (non-strict) or throws (strict)
config.newKey = "value"; // Ignored
console.log(config.timeout); // 5000 — unchanged
 
// Note: freeze is shallow
const nested = Object.freeze({
  outer: "frozen",
  inner: { value: 1 }, // inner object is NOT frozen
});
nested.inner.value = 99; // ✓ — inner object was not frozen

For deep immutability, you need recursive freeze or an immutability library.

Comparison Table

Featurevarletconst
ScopeFunctionBlockBlock
HoistedYes (initialized to undefined)Yes (TDZ — uninitialized)Yes (TDZ — uninitialized)
ReassignableYesYesNo
Value mutableYesYesYes (for objects/arrays)
Re-declarableYes (same scope)NoNo
TDZNoYesYes

When to Use Each

Use const by default — it communicates that you do not intend to reassign this variable. This is the clearest signal to both the engine and fellow developers.

Use let when the variable value needs to be updated:

javascriptjavascript
// let — reassigned in loop or accumulation
let total = 0;
for (const item of cart) {
  total += item.price; // Reassignment required
}
 
let currentUser = null;
currentUser = await fetchUser(); // Reassignment required

Never use var in modern JavaScript unless maintaining legacy code. var's function scope and hoisting to undefined are sources of bugs.

Rune AI

Rune AI

Key Insights

  • const prevents rebinding, not mutation: A const object or array can still have its contents changed; only reassigning the variable itself (= new value) is forbidden
  • Both let and const are hoisted into the TDZ: They exist in scope from the top of the block but throw ReferenceError if accessed before their declaration line — unlike var which initializes to undefined
  • let creates new bindings per loop iteration: Closures inside for loops with let each capture their own iteration variable — solving the classic setTimeout-in-loop bug
  • Object.freeze is needed for value immutability: To truly prevent mutation of a const object, wrap it with Object.freeze; freeze is shallow and does not affect nested objects
  • Default to const, use let only when needed: Communicates intent to developers and tooling; the lack of accidental reassignment makes code easier to reason about
RunePowered by Rune AI

Frequently Asked Questions

Does const improve performance meaningfully?

In most modern JavaScript engines, the difference between `let` and `const` is not significant enough to make a measurable performance impact on its own. The benefit is primarily about code clarity and intent signaling. The most important optimizations engines do are driven by runtime profiling (hidden class tracking, JIT compilation), not the `let`/`const` distinction.

Why can't I declare a const without initializing it?

`const` bindings are immutable — you cannot assign to them after declaration. Therefore, if you did not initialize at declaration, you would have an immutable `undefined` binding with no way to set it. The language requires initialization at declaration: `const x;` is a `SyntaxError`.

Is let safe to use inside loops with async/await?

Yes. Each iteration of a `for` loop with `let` creates a new binding. If you `await` inside the loop body, the closure over `let` captures the correct iteration value. With `var`, all iterations would share the same variable.

Does const prevent accidental reassignment in team code?

Yes — this is arguably its most important benefit. Code reviewers see `const` and know the variable should never be reassigned. If someone tries to reassign it, a runtime error immediately flags the mistake.

Conclusion

const and let share block scope and the temporal dead zone mechanism. The difference is that const prevents rebinding the variable to a new value — it does not prevent mutating the value itself (for objects and arrays). Use const by default for clarity and intent signaling; use let only when reassignment is genuinely needed. Avoid var in all modern code. Understanding the TDZ ensures you never accidentally access variables before their declaration line — a class of bug that var's silent undefined hoisting used to hide.