Menu
Try in Playground

JavaScript let vs const vs var: Scope, Hoisting, and When to Use Each

The three ways to declare a variable in JavaScript — and why modern code uses const by default, let when it has to, and var basically never.

Three Keywords, One Job

JavaScript has three ways to declare a variable: var, let, and const. They all bind a name to a value, but they differ in scope, reassignment rules, and how they behave before the declaration runs. Modern code uses const most of the time, let when a value needs to change, and var rarely if ever.

The short version:

index.js
Output
Click Run to see the output here.

The rest of this page is why those three lines behave the way they do.

const: The Default Choice

const creates a binding that can't be reassigned. Once you've said const x = 5, you can't later say x = 6 — the engine will throw a TypeError.

index.js
Output
Click Run to see the output here.

That's the whole rule. Because most variables in a well-written program don't need to be reassigned, const fits the vast majority of cases. Reaching for const first makes the places where a value does change stand out.

One common source of confusion: const protects the binding, not the value. If the value is an object or array, its contents are still mutable:

index.js
Output
Click Run to see the output here.

const means "this name always points at this object." It doesn't mean "this object never changes." If you need the object itself to be frozen, Object.freeze(user) is the tool — but in practice, most code just relies on the convention that const objects shouldn't be mutated.

let: When You Actually Need to Reassign

let is the same as const in every way except that you can reassign it. Use it for counters, accumulators, loop variables, and anywhere a value genuinely changes over time.

index.js
Output
Click Run to see the output here.

If you find yourself writing let and never reassigning the variable, switch it to const. The linter in most projects will nudge you.

Block Scope

Both let and const are block-scoped. A block is anything between { and } — the body of an if, a for, a function, or even a bare { ... } standalone block. A variable declared inside a block doesn't exist outside it.

index.js
Output
Click Run to see the output here.

This is what you want. It keeps variables scoped to the work they belong to, and makes accidental name collisions impossible.

var does not do this — which is the main reason to avoid it.

var: Function Scope and Surprises

var is scoped to the nearest function, not the nearest block. That means var declared inside an if or a for leaks out into the surrounding function:

index.js
Output
Click Run to see the output here.

That loose scoping is the source of a long list of classic JavaScript bugs — the famous one being loops where every iteration shares the same var i and all the callbacks end up seeing the final value.

var also lets you redeclare the same name in the same scope without complaint, which hides typos:

index.js
Output
Click Run to see the output here.

Modern code uses let and const almost exclusively. var shows up in legacy codebases, old Stack Overflow answers, and scripts that need to run in very old environments.

Hoisting and the Temporal Dead Zone

All three declarations are hoisted — the engine knows about them before it runs the code in the block — but they behave differently before the declaration line runs.

var is hoisted and initialized to undefined. You can reference it before the var line without an error:

index.js
Output
Click Run to see the output here.

let and const are hoisted but not initialized. Touching them before the declaration throws. This window between entering the scope and running the declaration is called the temporal dead zone (TDZ):

index.js
Output
Click Run to see the output here.

The TDZ is a feature, not a bug. It turns "used before declared" from a silent undefined into a loud error, which catches a lot of typos and ordering mistakes.

The const Loop Variable in for...of

A small but common case: for...of loops create a fresh binding on each iteration, so you can use const for the loop variable even though it "changes" between iterations.

index.js
Output
Click Run to see the output here.

Each iteration gets its own name binding — there's no single variable being reassigned. A classic for (let i = 0; i < n; i++) still needs let, because i is one binding that gets incremented.

A Practical Rule

Pick declarations with this order of preference:

  • const by default. If the value isn't going to be reassigned, say so.
  • let when the binding genuinely needs to change.
  • var only when working in a codebase that requires it.

Applied consistently, this makes the intent of every variable obvious at a glance — reassignable or not, scoped to this block or this function.

index.js
Output
Click Run to see the output here.

MAX_RETRIES and users never change — const. successful grows — let. user is a fresh binding each iteration — const. Reading top to bottom, you can tell which values move and which don't without running the code.

Next: Primitive Types

Now that you can declare variables, the next question is what kinds of values they can hold. JavaScript has a small set of primitive types — numbers, strings, booleans, and a few others — each with their own quirks. That's the next page.

Frequently Asked Questions

What's the difference between let, const, and var in JavaScript?

const and let are block-scoped and were added in ES2015; var is function-scoped and older. const bindings can't be reassigned, let bindings can. var is also hoisted and initialized to undefined, while let and const are hoisted but unusable until their declaration runs (the temporal dead zone).

Does const make a value immutable in JavaScript?

No. const only prevents reassignment of the binding. If the value is an object or array, you can still mutate its contents — const user = {}; user.name = 'Ada' works fine. Reach for Object.freeze or a library if you need actual immutability.

Should I still use var in modern JavaScript?

Almost never. let and const have clearer scoping rules and catch more bugs at parse time. The only places you'll meet var are legacy codebases and the occasional script that has to run in environments without ES2015 support.

Learn to code with Coddy

GET STARTED