var, let, and const — have meaningfully different behaviours around scoping, hoisting, and mutability. Getting these differences wrong is one of the most common sources of subtle bugs, especially in code written before ES2015 (ES6) introduced let and const.
The three keywords
var
var is the original variable declaration keyword, available since JavaScript was first created. It has two characteristics that make it tricky:
- Function-scoped — a
vardeclaration is visible throughout the entire function in which it appears, regardless of any inner blocks likeiforfor. - Hoisted and initialised to
undefined— the declaration is moved to the top of its containing function before any code runs, but its assigned value stays in place.
let
let was introduced in ES2015 and is block-scoped. Its declaration is also hoisted, but unlike var it is not initialised — accessing it before the assignment throws a ReferenceError. The region between the start of the block and the let declaration is called the temporal dead zone (TDZ).
const
const also has block scope and a temporal dead zone, but it additionally requires that you provide an initial value at the point of declaration and prevents reassignment of the binding.
const prevents reassigning the variable binding, not mutations to the value itself. If the value is an object or array, you can still modify its properties or elements.Scope
Scope determines where in your code a variable is accessible. JavaScript has three main scope levels.Global scope
Variables declared outside any function or block exist in the global scope and are accessible everywhere.Function scope
Each function creates its own scope. Variables declared withvar, let, or const inside a function are not accessible from outside it.
Block scope
A block is any pair of{} curly braces — an if body, a for loop, or a standalone block. Variables declared with let or const inside a block are only accessible within that block. var ignores block boundaries.
let is preferred over var for loop counters: with var, the counter leaks out of the loop and can cause surprising behaviour, particularly in closures.
Hoisting in detail
Hoisting is the mechanism by which variable and function declarations are processed before any code runs within their scope. It is useful to think of it as the engine making two passes: first collecting all declarations, then executing the code.Function declarations are fully hoisted
The entire function — name and body — is available anywhere in its scope, even before the line where it was written.
var declarations are hoisted but not their values
The variable name is registered at the top of the function scope and initialised to
undefined. The value assignment stays in place.let and const are hoisted but enter the temporal dead zone
The engine knows about the variable (it is hoisted), but it refuses to allow access until the declaration line is reached. Any access in the TDZ throws a
ReferenceError.Choosing between var, let, and const
In modern JavaScript, the guidance is clear:- Use
constby default for any binding whose value you do not intend to reassign. - Use
letwhen you need to reassign the variable (loop counters, accumulation patterns). - Avoid
varin new code. Its function-scoping and initialisation-to-undefinedhoisting behaviour are rarely what you want and make code harder to reason about.
Quick reference
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes (initialised to undefined) | Yes (TDZ — not accessible) | Yes (TDZ — not accessible) |
| Reassignable | Yes | Yes | No |
| Re-declarable in same scope | Yes | No | No |
| Available before declaration | Yes (undefined) | No (ReferenceError) | No (ReferenceError) |