In JavaScript, which redeclarations of the same name does var allow that let and const reject, and when does declaring a name in a nested block become a SyntaxError rather than legal shadowing?
answer
- one keyword tolerates duplicates
- shadowing is not redeclaring
- the asymmetric nesting trap
- parse-time, so nothing runs
basics
~20 svar silently allows the same name twice in one scope; let and const make it a SyntaxError. Shadowing the same name in a nested block is always legal for let and const, but a var inside the block collides with an outer let because var is hoisted past the braces.
solid answer
~50 sWithin one scope, `var x = 1; var x = 2;` is legal and the second declaration is simply ignored — only the assignment runs. The same code with `let` or `const` is a SyntaxError, `Identifier 'x' has already been declared`, and so is any mix of the two in one scope: `var x; let x;` and `let x; var x;` both fail. Because it is a *parse* error, the whole script or module refuses to run, not just that statement. Shadowing is different and always allowed: an inner block may declare `let x` even though an outer scope has one, and the inner binding wins for the length of the block. The trap is that `var` does not respect the block — in `let x = 1; { var x = 2; }` the `var` hoists into the same function scope as the outer `let` and produces a SyntaxError even though the two declarations look nested.
code
javascript · 7 linesfunction f() {
var x = 1;
var x = 2; // legal: one binding, second declaration ignored
var x; // no initializer, so nothing is written
return x; // 2
}
console.log(f()); // 2go deeper
Know that repeating var in one scope is allowed while repeating let or const is an error, and that declaring the same name inside a nested block is normal shadowing, not an error.
Explain that duplicate lexical declarations are caught at parse time so the file never runs, and demonstrate why an inner var collides with an outer let while an inner let does not collide with an outer var.
Use the diagnostic angle: distinguish a silent parse failure from a runtime throw when triaging, and explain why converting legacy var code can surface collisions with parameters and switch clauses.
Own the policy call — banning var and enabling no-shadow style rules trades some local convenience for a class of bugs the parser can catch, and you should be able to defend where that tradeoff is worth enforcing.
## The rule in one sentence A scope may contain at most one **lexical** declaration (`let`, `const`, `class`) of a name, and a lexical declaration may not coexist with a `var` of the same name in that scope. `var` alone may repeat freely. ## var redeclaration is a no-op ```js function f() { var x = 1; var x = 2; // legal — one binding, two assignments console.log(x); // 2 } ``` There is only ever one binding for `x` in `f`. The second `var` re-declares a name that already exists, which the language treats as nothing at all; the initializer still executes as an ordinary assignment. Crucially, a bare `var x;` after an assignment does *not* reset the value: ```js var y = 1; var y; console.log(y); // 1 — the second declaration carries no initializer, so nothing is written ``` That tolerance is the historical reason `var` is dangerous in long functions: reusing a name for a second purpose compiles silently and produces wrong values. ## Lexical redeclaration is a SyntaxError ```js let a = 1; let a = 2; // SyntaxError: Identifier 'a' has already been declared ``` All of these fail the same way, in either order: ```js let b; const b = 1; // SyntaxError var c; let c; // SyntaxError let d; var d; // SyntaxError class E {} let E; // SyntaxError ``` Because this is detected while the code is being parsed, the failure is not local: the entire script or module containing it never executes, so no output at all appears from lines above the offending one. That is a distinct diagnostic signature and worth recognizing — "nothing ran" points at a parse error, whereas "it ran and then threw" points at a runtime one. Function parameters count as declarations of the function's top-level scope, so: ```js function g(p) { let p; } // SyntaxError function h(p) { var p; } // legal — var p re-declares the parameter and keeps its value ``` ## Shadowing is a different question Shadowing means declaring the same name in an *inner* scope. It is always legal for `let` and `const`, and the inner binding hides the outer one for the length of the block: ```js let n = 'outer'; { let n = 'inner'; console.log(n); // "inner" } console.log(n); // "outer" — untouched ``` The two bindings are genuinely separate slots; assigning to the inner one cannot affect the outer one. The same holds for function parameters shadowing outer names, and for a `catch` clause parameter shadowing a variable of the same name. ## Where shadowing turns into a collision The subtlety is that a `var` inside a block is not *in* that block — it belongs to the enclosing function. So a `var` that looks like it is shadowing is actually declaring in the same scope as the outer binding: ```js let k = 1; { var k = 2; // SyntaxError: Identifier 'k' has already been declared } ``` The `var k` hoists to the enclosing function or script scope, where `let k` already lives, and the two cannot coexist. Reverse the keywords and it is fine, because the `let` really does stay in the block: ```js var m = 1; { let m = 2; // legal — a genuinely nested binding console.log(m); // 2 } console.log(m); // 1 ``` This asymmetry is the single most confusing consequence of mixing the two declaration forms, and it is why the practical advice is to not mix them at all. ## Switch clauses share one scope ```js switch (v) { case 1: let s = 'a'; break; case 2: let s = 'b'; break; // SyntaxError — same block } ``` A `switch` body is a single block, so both `let s` declarations land in the same scope. Wrapping each clause's statements in its own braces (`case 1: { let s = 'a'; break; }`) gives each one a real block and fixes it. ## What to say in an interview State the rule (one lexical declaration per name per scope; `var` may repeat), then name the phase (SyntaxError at parse, so nothing in the file runs), then give the shadowing asymmetry as the interesting case: `let` outside plus `var` inside collides, `var` outside plus `let` inside does not. Finish with the practical consequence — the error class that `let` turns into a build failure is exactly the class that `var` used to turn into a silent wrong answer.
- Is a function parameter treated as a declaration for these collision rules?Yes. Parameters are bindings of the function's top-level scope, so `function g(p) { let p; }` is a SyntaxError, while `function h(p) { var p; }` is legal and leaves the parameter's value intact. The same applies to `const`. It is a common surprise when someone converts a body-level `var` to `let` and hits an error on a name that shadowed a parameter.
- If a redeclaration is a SyntaxError, do the statements above it still run?No. Duplicate-declaration checks happen when the script or module is parsed, before any of it executes, so nothing in the file runs — not even a console.log on line one. That signature is diagnostic: total silence means a parse error, while output followed by a throw means a runtime error such as a const reassignment.
- Why do two case clauses in one switch collide when they look like separate branches?Because the entire switch body is one block, not one block per clause. Both `let` declarations therefore land in the same scope and the second is a duplicate. Giving each clause its own braces — `case 1: { let s = 'a'; break; }` — creates real per-clause blocks and resolves it.
saying these in an interview costs you the question
- Says redeclaring a let just overwrites the old value
- Claims duplicate var throws at runtime
- Thinks shadowing and redeclaration are the same thing
- Believes a var inside braces shadows an outer let
- Says a redeclaration error only skips that one line