JAVASCRIPT / VARIABLES: VAR, LET, AND CONST
Reassignment, redeclaration, and const limits
You can tell reassignment, redeclaration, and mutation apart, predict whether a failure is a TypeError or a SyntaxError, and know what const really protects.
What you will learn
- Reassign let bindings freely; assigning to a const binding throws a TypeError
- Spot duplicate let/const names as a SyntaxError that stops the file from running
- Mutate objects held in const bindings; use Object.freeze to block writes
- Use const in for-of loops but let for counters you increment with i++
Understanding Reassignment, redeclaration, and const limits
Reassignment and redeclaration are different operations, and the engine checks them at different moments. Reassignment writes a new value into a binding that already exists: total = 2 requires total to exist and to be writable, and that requirement is tested when the line runs. Redeclaration introduces the same name twice in one scope, and that is decided while the engine is still parsing, before any statement executes. var allows both, because a second var of the same name is folded into the first declaration; let allows reassignment only; const allows neither.
Since duplicate lexical declarations are a SyntaxError, there is nothing to defend against at runtime. While parsing a scope the engine collects every let and const name it declares, and a second declaration of that name, including let x followed by var x in the same function, makes the whole file unparseable, so not one line of it runs. The same parse-time rule explains why const x; is rejected: a const binding can never be written to again, so the initializer is the only chance to supply a value and the grammar insists on it.
const constrains the binding, not the value. A binding is the slot that maps a name to a value; const marks that slot read-only, and for a number or a string this looks like immutability only because primitives have no mutable parts. When the value is an object the slot holds a reference, so config.retries = 5 and list.push(x) never touch the slot and succeed, while config = {} is rejected. Protecting the contents is Object.freeze's job, and freeze is shallow, so nested objects stay writable.
let total = 1;
total = 2;
console.log("let reassigned:", total);
var mode = "dev";
var mode = "prod";
console.log("var redeclared:", mode);
const limit = 10;
try {
limit = 20;
} catch (err) {
console.log("const reassigned:", err.name);
}
const settings = { retries: 3 };
settings.retries = 5;
console.log("const object mutated:", settings.retries);const locks the name-to-value binding rather than the value, and duplicate declarations are rejected by the parser while illegal assignments fail only when their line runs.
Worked examples
Redeclaration fails before anything runs
Compiling snippets with the Function constructor turns parse-time errors into values you can inspect.
const snippets = [
"var a = 1; var a = 2;",
"let b = 1; let b = 2;",
"let c = 1; var c = 2;",
"const d;"
];
for (const src of snippets) {
try {
new Function(src);
console.log("parsed:", src);
} catch (err) {
console.log(err.name + ":", src);
}
}Example explained
Line 1new Function compiles its body immediately, so the parse error arrives as a catchable object instead of killing the surrounding file.
Line 2The two var declarations parse because the second one reuses the binding the first created.
Line 3let b twice, and let c followed by var c, are duplicate declarations in one scope, so both are rejected before execution.
Line 4const d has no initializer, and a const binding can never be assigned afterwards, so the grammar refuses it.
const objects, Object.freeze, and strict mode
Shows that const stops rebinding while Object.freeze is what stops property writes.
"use strict";
const scores = [10, 20];
scores.push(30);
console.log("scores:", scores.join(","));
const point = Object.freeze({ x: 1, y: 2 });
try {
point.x = 99;
} catch (err) {
console.log("frozen write:", err.name);
}
console.log("point.x is still", point.x);Example explained
Line 1scores.push mutates the array while the const binding keeps pointing at the same array, so nothing const guards is touched.
Line 2Object.freeze makes the existing properties non-writable, which is a property-level restriction, not a binding-level one.
Line 3Under "use strict" the rejected write to point.x throws; without strict mode the same assignment would be discarded silently.
Line 4The final log proves const never protected x, and that freeze is what kept the value at 1.
const in loop heads
Explains why for-of accepts const but a counting for loop does not.
for (const item of ["a", "b"]) {
console.log("item:", item);
}
try {
for (const i = 0; i < 3; i++) {
console.log("i:", i);
}
} catch (err) {
console.log("counter loop stopped:", err.name);
}Example explained
Line 1for-of creates a fresh binding for each element, so const is initialized rather than reassigned.
Line 2The counting loop declares i once, and i++ is an assignment to that same const binding.
Line 3The body runs once with i as 0, which is why partial output appears before the increment throws.
Line 4Swapping const for let in the second head is the whole fix; the loop is otherwise valid syntax.
Important notes
An illegal assignment is always a TypeError, but the message text is engine-specific: V8 reports "Assignment to constant variable." while SpiderMonkey reports an invalid assignment to const. Check the type, never the wording.
Declaring the same name inside a nested block is not redeclaration; it creates a separate binding that shadows the outer one. Only two declarations in the same scope collide.
Common mistakes
Treating const user = { ... } as immutable data, then hunting for why user.role changed: const only blocked rebinding the name, so every property write from anywhere else succeeded.
Wrapping a duplicate let in try/catch to handle it, when the SyntaxError is raised during parsing, so the file never executes and the catch block is never reached.
Writing const result; and planning to assign it inside an if/else, which fails with a missing-initializer SyntaxError; use let, or compute the value with a ternary or a function call.
Try it yourself
Change, predict, then run
In a browser console create const cart = { items: [], total: 0 }, push two items and update cart.total, then try cart = { items: [] } and note the error type. Now call Object.freeze(cart) and check whether cart.total = 5 and cart.items.push("c") still work.
Open the JavaScript workspaceCheck your understanding
A file logs "start" on its first line and, twenty lines later, contains let total = 1; let total = 2; wrapped in a try/catch. What does running the file print?
- "start", then the name of the caught error
- "start", then nothing, because the engine stops at the duplicate declaration
- Nothing at all
- "start" and 1, then a TypeError from the second declaration
Show answer
A duplicate let declaration is an early error found while parsing, so the engine rejects the entire file and never reaches the first line, and try/catch can only intercept errors thrown by code that is actually running. The first option is the trap: it assumes the duplicate behaves like an illegal const assignment, which really is a runtime TypeError thrown when its line executes.