skip to content

var vs let and const

Why var leaks out of if-blocks and loops while let and const stay inside the braces, and what const actually protects. Interviewers want to hear that const freezes the binding, not the object it points at.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In JavaScript, why does a variable declared with var inside an if block stay visible after that block ends, while the same variable declared with let does not?

level: juniorimportance: must knowfreq 85%

answer

  1. braces versus function boundary
  2. one keyword ignores blocks
  3. the if-block leak
  4. nearest function, not nearest brace

basics

~20 s

var binds to the nearest enclosing function (or the script itself), so blocks are invisible to it. let and const bind to the nearest pair of braces, so their names stop existing when that block ends.

solid answer

~50 s

The keyword decides which scope the binding is created in, and that decision is made when the code is parsed. `var` creates its binding in the nearest enclosing **function** — or in the global/script scope if there is no function — so an `if`, `for`, `while`, `switch` or bare `{ }` block simply does not contain it. `let` and `const` create their binding in the nearest enclosing **block**, so every pair of braces gets its own. That is why `if (true) { var a = 1; }` leaves `a` readable for the rest of the function, while `if (true) { let b = 2; }` makes `b` a ReferenceError one line later. Function boundaries still stop `var`: a `var` inside a nested function never leaks to the outer one. In practice this is why modern code defaults to `const`, uses `let` when it needs to reassign, and avoids `var` entirely.

go deeper

for a junior

Be able to say plainly that var is scoped to the enclosing function while let and const are scoped to the nearest braces, and predict the output of a short if-block or for-loop example that mixes them.

for a middle

Explain that the scope is chosen at parse time by the keyword, that a switch body is one block rather than one per case, and show how to restructure code that relied on a var declared inside a conditional.

for a senior

Show the judgment behind a codebase rule: const by default, let only for real reassignment, var banned by lint. Be ready to explain what class of production bug narrower scoping actually removes.

for a principal

Own the migration argument — why converting legacy var code is worth the churn, how to sequence it so a scope change never silently alters behaviour, and where an automated codemod is safe versus where it needs review.

## What a declaration actually does Every declaration creates a *binding*: a name attached to a storage slot inside some environment record. The keyword you use decides **which** environment record gets that binding, and that choice is fixed statically when the source is parsed — nothing at runtime can move it. JavaScript has two answers to "which record?": - `var` binds into the nearest enclosing **function** scope. If there is no enclosing function, it binds into the script/global scope. Blocks — `if`, `for`, `while`, `switch`, `try`/`catch`, or a bare `{ }` — are not var scopes at all. - `let` and `const` bind into the nearest enclosing **block**. Every pair of braces creates a fresh block environment, including the head of a `for` loop and the body of a `switch`. ```js function f() { if (true) { var a = 1; let b = 2; } console.log(a); // 1 — a was never inside the if block; it belongs to f console.log(b); // ReferenceError: b is not defined } ``` The mental model that keeps this straight: with `var`, the braces are decoration; the only thing that carves out a new scope is a `function` boundary. ## Why var works this way Before ES2015 the language had no block-scoped declaration form at all — `var` and function parameters were the whole story, and function scope was the only granularity available. `let` and `const` were added in ES2015 with block scoping precisely because function-only scoping is a poor fit for the way people write loops and conditionals. `var` kept its old rules unchanged, because changing them would silently alter the meaning of existing code. ## Loops make the difference obvious ```js for (var i = 0; i < 3; i++) { /* ... */ } console.log(i); // 3 — i survives the loop for (let j = 0; j < 3; j++) { /* ... */ } console.log(j); // ReferenceError: j is not defined ``` With `var`, the counter is a variable of the whole function that the loop happens to use. With `let`, the counter belongs to the loop and nothing outside can see it. In a long function, a stray `var i` from a loop two hundred lines up is a name you can accidentally reuse or clobber; with `let` the engine tells you the name does not exist there. ## Blocks people forget are blocks A bare block is a real scope: ```js { let secret = 1; var open = 2; } console.log(typeof open); // "number" console.log(typeof secret); // "undefined" — secret never existed out here ``` A `switch` statement body is **one** block, not one block per `case`, so two `case` clauses cannot each declare `let x` — that is a duplicate declaration in the same scope. A `catch` clause's parameter is bound to the catch block and disappears with it. ## Functions still stop var `var` climbs out of blocks, but never out of a function: ```js function outer() { function inner() { var hidden = 1; } inner(); console.log(hidden); // ReferenceError } ``` This is the reason the old immediately-invoked-function idiom existed: wrapping code in a function was the only way to give `var` a boundary. With `let` and `const`, a plain `{ }` does the same job. ## Where this bites in real code The classic bug is a name reused for two purposes in one long function. With `var`, the second use quietly overwrites the first — the program keeps running and produces a wrong number. With `let`, the second declaration in the same scope is a hard error at parse time and the file does not run at all, which is exactly the feedback you want. The second common bug is a value computed inside a conditional and read afterwards. Code written with `var` gets away with declaring it inside the `if`; the same code converted to `let` breaks, and the correct fix is to declare the binding in the outer scope and assign inside the branch: ```js let result; if (condition) { result = compute(); } else { result = fallback(); } ``` ## The working rule Use `const` by default, `let` when the binding genuinely needs to be reassigned, and `var` essentially never in new code. `const` and `let` give you the narrowest scope the language can express, and the narrower the scope, the fewer lines of code can possibly be responsible when a value is wrong.

  • If var ignores blocks, does it also escape a nested function?
    No. `var` climbs out of any block but stops at the nearest function boundary — a `var` declared inside a nested function is invisible to the enclosing one. That is exactly why the pre-ES2015 idiom for creating a private scope was to wrap code in a function and call it immediately; with `let` and `const` a bare `{ }` block does the same job.
  • Do two case clauses in a switch each get their own scope?
    No — the whole `switch` body is a single block. So `case 1: let x = 1; break; case 2: let x = 2;` is a duplicate declaration in one scope and a SyntaxError. The usual fix is to wrap each case's statements in their own braces, `case 1: { let x = 1; break; }`, which gives every clause a real block.
  • Is a bare pair of braces at statement position a real scope?
    Yes. `{ let a = 1; var b = 2; }` creates a block environment: `a` exists only inside it, while `b` belongs to the enclosing function and outlives the braces. It is a legitimate way to keep a short-lived helper binding from polluting the rest of a function, and it costs nothing at runtime.

saying these in an interview costs you the question

  • Says var and let differ only in style, not scope
  • Claims a bare block does not create a scope
  • Thinks var leaks out of nested functions too
  • Says let is function-scoped but var is global
  • Believes the difference only shows up in loops

context

open as a page

In JavaScript, what exactly does const protect — and why can you still call push() on an array that was declared with const?

level: middleimportance: must knowfreq 80%

basics

~10 s

const 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.

open as a page

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?

level: middleimportance: should knowfreq 55%

basics

~20 s

var 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.

open as a page

At the top level of a classic (non-module) script, var declarations become properties of globalThis but let and const do not. What problems does that cause, and how does the picture change in an ES module?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Top-level var in a classic script creates a non-configurable property on the global object, so separate scripts can collide with each other and with built-in globals. let and const instead create bindings in a separate global scope that is not reachable as a property. In an ES module neither touches globalThis.

open as a page