In JavaScript, what exactly does const protect — and why can you still call push() on an array that was declared with const?
answer
- the name, not the contents
- a label, not a lock
- re-pointing versus mutating
- TypeError on assignment, not on push
basics
~10 sconst makes the binding unchangeable, not the value. The name can never be pointed at a different value, but if that value is an object or array its contents stay fully mutable.
solid answer
~50 s`const` is a rule about the **binding**, not about the data. It says the name may never be re-bound after initialization, so `arr = somethingElse` throws `TypeError: Assignment to constant variable`. It says nothing about the object the name currently points at, so `arr.push(3)`, `obj.key = 1` and `delete obj.key` all work exactly as they would with `let`, because none of those touch the binding — they mutate the object it references. The other half of the rule is that a `const` declaration must be initialized where it is written: `const x;` is a SyntaxError, `Missing initializer in const declaration`. Note the two failures land at different times — a missing initializer is caught when the code is parsed, whereas reassignment is a runtime `TypeError`. For a primitive the distinction is invisible, since there is nothing to mutate; for objects it is the single most common misunderstanding of the keyword.
code
javascript · 10 linesconst nums = [1, 2];
nums.push(3);
console.log(nums); // [1, 2, 3]
try {
nums = [];
} catch (err) {
console.log(err.constructor.name); // "TypeError"
console.log(err.message); // "Assignment to constant variable."
}go deeper
Know the one-line rule: const locks the name, not the contents, so pushing to a const array works while assigning a new array to it throws. Be able to show both in two lines of code.
Explain the binding-versus-value distinction in your own words, name the exact error text and phase for each failure, and say why a const declaration must carry an initializer.
Demonstrate the operational point: const buys you the guarantee that a name means one thing throughout its scope, and it gives you nothing at an API boundary where a callee may mutate what you passed.
Own the codebase-wide argument — where const-by-default genuinely reduces defect rates, and where a team's real need is an immutability discipline at module boundaries that no declaration keyword can enforce.
## Two different things a language could freeze When a name refers to an object there are two separate things that could, in principle, be made immutable: 1. the **binding** — the association between the name and the value it refers to; 2. the **value** — the contents of the object itself. `const` addresses only the first. It is, precisely, a declaration whose binding cannot be re-assigned after it is initialized. The object on the other end is an ordinary mutable object. ```js const nums = [1, 2]; nums.push(3); // fine — the binding still points at the same array console.log(nums); // [1, 2, 3] nums = []; // TypeError: Assignment to constant variable. ``` The first statement mutates the array; the binding never changes. The last statement tries to make the name refer to a *different* array, which is exactly what `const` forbids. ## The same rule stated as an operation test A useful test: ask whether the operation writes to the **name** or through it. ```js const config = { retries: 3 }; config.retries = 5; // writes through the name — allowed config.timeout = 1000; // adds a property — allowed delete config.retries; // removes a property — allowed Object.assign(config, {}); // mutates in place — allowed config = { retries: 5 }; // writes to the name — TypeError ``` Everything except the last line leaves the binding untouched. ## Primitives hide the distinction ```js const n = 1; n = 2; // TypeError ``` With a number, a string, a boolean, `null`, `undefined`, a symbol or a bigint there is nothing to mutate — primitives have no mutable state — so `const` on a primitive *looks* like it froze the value. It did not; there was simply no second thing to freeze. Interviewers use the object case precisely because it separates the two ideas that the primitive case conflates. ## The initializer rule A `const` declaration must supply a value at the point of declaration: ```js const x; // SyntaxError: Missing initializer in const declaration let y; // fine — y is undefined until assigned ``` That follows directly from the binding rule: if you could declare without initializing, the one assignment `const` allows would have to happen later, and the language would need to track whether it had happened yet. Forbidding the form removes the question. Notice that the two failure modes occur at different phases. The missing initializer is a **SyntaxError** — the script or module containing it never runs at all, even if the line would have been unreachable. Reassignment is a **TypeError** thrown at the moment the assignment executes, so code before it runs normally. ## Destructuring and loop heads The rule applies per binding created, not per statement: ```js const { a, b } = obj; // two const bindings, both un-reassignable const [first] = arr; // one const binding ``` In loop heads: ```js for (const item of list) { /* ... */ } // fine — a fresh binding each iteration for (const i = 0; i < 3; i++) { } // TypeError on i++ — the same binding is reassigned ``` `for...of` and `for...in` create a new binding for every iteration, so `const` is legal and is the usual choice. A C-style `for` head keeps one binding and updates it, so the update expression violates `const` on the very first pass; that loop needs `let`. ## What const does buy you It is easy to conclude that `const` is nearly worthless because it does not protect data. That is the wrong lesson. What it guarantees is that **wherever you see the name, it refers to the same value it was given on the declaration line** — you can read one line and know what the name denotes for the rest of its scope, without scanning for reassignments. That is a real reduction in the amount of code you must hold in your head, and it is why `const` is the sensible default and `let` the deliberate exception. What `const` does *not* give you is any protection against a function you hand the object to mutating it. If you need that guarantee, the binding keyword is the wrong tool entirely — you need either a value the callee cannot change or a copy handed out at the boundary. Saying so out loud is what separates a candidate who has memorized "const isn't deep" from one who understands why.
- Is reassigning a const a parse-time or a run-time failure?Run time. `x = 1` on a const binding throws `TypeError: Assignment to constant variable.` when that statement executes, so everything before it runs normally. The other const rule fails earlier: omitting the initializer, as in `const x;`, is a SyntaxError, and a file containing it never executes at all — even if the line would have been unreachable.
- Why is const legal in a for...of head but not in a classic for head?`for...of` and `for...in` create a fresh binding for each iteration, so nothing is ever reassigned and `const` is both legal and the idiomatic choice. A C-style `for (const i = 0; i < 3; i++)` keeps a single binding and the `i++` update reassigns it, which throws a TypeError on the first pass. That loop needs `let`.
- Does const behave differently for a primitive than for an object?The rule is identical — the binding cannot be re-pointed either way. The difference is only that primitives have no mutable state, so there is nothing you could change through the name; with an object there is, which is why the object case exposes the distinction and the primitive case hides it.
const is like gluing a label onto one box: you can never peel the label off and stick it on a different box, but nothing stops anyone from opening that box and rearranging what is inside.
saying these in an interview costs you the question
- Says const makes the object immutable
- Claims push on a const array throws
- Thinks const is just let with better style
- Believes const only works for primitives
- Says reassigning a const is a SyntaxError