var vs let vs const: JS Variable Declarations
Understand the key differences between var, let, and const in JavaScript. This guide covers scope, hoisting, the temporal dead zone, reassignment rules, and when to use each keyword with practical code examples.
JavaScript gives you three keywords for declaring variables: var, let, and const. All three create named storage for values, but they behave differently in critical ways that affect your code's correctness, readability, and safety. Choosing the wrong keyword can introduce subtle bugs that only surface under specific conditions, while choosing the right one communicates clear intent about how a variable is meant to be used.
Think of it this way: const is a locked safe deposit box (the label is permanently assigned to the box, though you can rearrange the contents inside), let is a labeled drawer that you can empty and refill as needed, and var is a sticky note that might end up on the wrong desk if you are not careful. This guide explains exactly how each keyword works, where they differ, and which one to reach for in every situation.
The Quick Comparison
Before diving deep, here is the definitive comparison table:
| Feature | var | let | const |
|---|---|---|---|
| Introduced | ES1 (1997) | ES6 (2015) | ES6 (2015) |
| Scope | Function | Block ({}) | Block ({}) |
| Reassignment | Yes | Yes | No |
| Redeclaration (same scope) | Yes | No (SyntaxError) | No (SyntaxError) |
| Hoisted | Yes (initialized as undefined) | Yes (uninitialized, TDZ) | Yes (uninitialized, TDZ) |
Attaches to window (global) | Yes | No | No |
| Requires initialization | No | No | Yes |
Scope: The Most Important Difference
Scope determines where a variable is accessible. This is the single biggest behavioral difference between var and let/const.
var: Function Scope
var is scoped to the nearest function. It ignores block boundaries like if, for, and while.
function processOrder(orderTotal) {
if (orderTotal > 100) {
var discount = 0.15; // Declared inside if-block
var message = "Premium discount applied!";
}
// Both variables are accessible here because var is function-scoped
console.log(discount); // 0.15 (leaks out of the if-block)
console.log(message); // "Premium discount applied!"
}
processOrder(150);// Classic var-in-loop problem
for (var i = 0; i < 5; i++) {
// i is function-scoped, not loop-scoped
}
console.log(i); // 5 (i leaks out of the loop!)let and const: Block Scope
let and const are scoped to the nearest block (any pair of curly braces {}).
function processOrder(orderTotal) {
if (orderTotal > 100) {
let discount = 0.15; // Block-scoped
const message = "Premium!"; // Block-scoped
console.log(discount, message); // Works: inside the block
}
// console.log(discount); // ReferenceError: discount is not defined
// console.log(message); // ReferenceError: message is not defined
// Both variables are contained to the if-block
}// let in loops: each iteration gets its own 'i'
for (let i = 0; i < 5; i++) {
// i is scoped to this loop block
}
// console.log(i); // ReferenceError: i is not definedBlock Scope is Safer
Block scope means variables only exist where they are needed. Function scope means variables can be accidentally used in parts of the function where they were not intended to be used. This is why let and const produce fewer bugs than var.
Hoisting: What Happens Before Code Runs
All three keywords hoist their declarations to the top of their scope, but they behave differently during the hoisting phase.
var Hoisting: Initialized as undefined
When JavaScript hoists a var declaration, it initializes the variable with undefined before any code runs. This means you can reference a var variable before its declaration line without getting an error.
console.log(greeting); // undefined (not an error!)
var greeting = "Hello";
console.log(greeting); // "Hello"
// JavaScript interprets this as:
// var greeting = undefined; <-- hoisted and initialized
// console.log(greeting); <-- undefined
// greeting = "Hello"; <-- assignment stays in place
// console.log(greeting); <-- "Hello"let and const Hoisting: The Temporal Dead Zone
let and const are also hoisted, but they are NOT initialized. The region between the top of the scope and the declaration line is called the Temporal Dead Zone (TDZ). Accessing the variable inside the TDZ throws a ReferenceError.
// console.log(name); // ReferenceError: Cannot access 'name' before initialization
let name = "Alice"; // TDZ ends here
console.log(name); // "Alice"
// Same behavior with const:
// console.log(MAX); // ReferenceError: Cannot access 'MAX' before initialization
const MAX = 100;
console.log(MAX); // 100// Visualizing the Temporal Dead Zone
{
// ---- TDZ for 'score' starts here ----
// Any access to 'score' in this zone throws ReferenceError
console.log("Setting up...");
// console.log(score); // ReferenceError!
let score = 95;
// ---- TDZ for 'score' ends here ----
console.log(score); // 95 (safe: past the declaration)
}| Keyword | Hoisted? | Value Before Declaration | Access Before Declaration |
|---|---|---|---|
var | Yes | undefined | Returns undefined (no error) |
let | Yes | Uninitialized (TDZ) | ReferenceError |
const | Yes | Uninitialized (TDZ) | ReferenceError |
The TDZ Is Actually a Feature
It might seem inconvenient that let and const throw errors when accessed too early, but this is intentional. Accessing a variable before it is declared is almost always a bug. The TDZ catches this mistake at runtime instead of silently giving you undefined.
Reassignment and Redeclaration
Reassignment
Reassignment means changing the value a variable holds after its initial declaration.
// var: reassignment allowed
var score = 10;
score = 20; // Fine
// let: reassignment allowed
let level = 1;
level = 2; // Fine
// const: reassignment NOT allowed
const name = "Alice";
// name = "Bob"; // TypeError: Assignment to constant variableRedeclaration in the Same Scope
Redeclaration means declaring a variable with the same name in the same scope.
// var: redeclaration allowed (this causes real bugs)
var color = "red";
var color = "blue"; // No error! Silently overwrites
console.log(color); // "blue"
// let: redeclaration NOT allowed
let size = "large";
// let size = "small"; // SyntaxError: Identifier 'size' has already been declared
// const: redeclaration NOT allowed
const MAX = 100;
// const MAX = 200; // SyntaxError: Identifier 'MAX' has already been declaredThe fact that var allows redeclaration in the same scope is one of its biggest footguns. In a long function with many lines, you might accidentally reuse a variable name and silently overwrite the original value.
The Classic Loop Closure Problem
This is the most famous example of why var causes real-world bugs. It surfaces in any code that combines loops with asynchronous callbacks.
// BUG: using var in a loop with setTimeout
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(`var: ${i}`);
}, 100);
}
// Output: "var: 3", "var: 3", "var: 3"
// All three callbacks share the same 'i', which is 3 after the loop ends
// FIX: using let in a loop with setTimeout
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(`let: ${i}`);
}, 100);
}
// Output: "let: 0", "let: 1", "let: 2"
// Each iteration gets its own copy of 'i'Why does this happen? With var, there is only one i variable for the entire function. All three callbacks reference the same i, which has the value 3 after the loop completes. With let, each iteration of the loop creates a fresh i that the callback captures independently.
// Real-world version of this bug: attaching click handlers
const buttons = document.querySelectorAll(".tab-button");
// BUG with var
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log(`Clicked tab ${i}`); // Always logs the last index
});
}
// FIX with let
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log(`Clicked tab ${i}`); // Logs the correct index
});
}const with Objects and Arrays
A common misconception is that const makes values immutable. It does not. const prevents reassignment of the variable binding, but it does not prevent mutation of the value.
// const with primitives: effectively immutable
const pi = 3.14159;
// pi = 3.14; // TypeError: Assignment to constant variable
// const with objects: the reference is constant, contents are mutable
const user = { name: "Alice", age: 28 };
user.age = 29; // Allowed: mutating a property
user.email = "a@test.com"; // Allowed: adding a property
delete user.email; // Allowed: deleting a property
console.log(user); // { name: "Alice", age: 29 }
// user = { name: "Bob" }; // TypeError: can't reassign the binding
// const with arrays: the reference is constant, elements are mutable
const colors = ["red", "green", "blue"];
colors.push("yellow"); // Allowed: mutating the array
colors[0] = "crimson"; // Allowed: changing an element
console.log(colors); // ["crimson", "green", "blue", "yellow"]
// colors = ["purple"]; // TypeError: can't reassign the bindingIf you need a truly immutable object, use Object.freeze():
const config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
});
config.timeout = 10000; // Silently fails (or throws in strict mode)
console.log(config.timeout); // 5000 (unchanged)The Global Object Difference
// var declarations at the global level attach to window
var globalVar = "I'm on window";
console.log(window.globalVar); // "I'm on window"
// let and const do NOT attach to window
let globalLet = "I'm NOT on window";
const globalConst = "I'm NOT on window either";
console.log(window.globalLet); // undefined
console.log(window.globalConst); // undefinedThis matters because third-party scripts that check window.someVariable will only find variables declared with var. With let and const, the global namespace stays cleaner.
Decision Framework: Which Keyword to Use
| Situation | Use | Reasoning |
|---|---|---|
| Value never changes after declaration | const | Signals immutability of the binding to readers |
| Value needs to be reassigned | let | Block-scoped, no hoisting surprises |
| Loop counter variable | let | Each iteration gets its own copy |
| True application constant | const + UPPER_SNAKE_CASE | const MAX_RETRIES = 3; |
| Legacy code or frameworks that require it | var | Only when absolutely necessary |
| Accumulator (sum, counter, builder) | let | Starts at initial value, changes over time |
| Function declaration | const + arrow | const add = (a, b) => a + b; prevents reassignment |
Best Practices
Default to const for everything. Start every variable declaration with const. Only change to let when the linter (or your logic) tells you a reassignment is needed. This makes reassignment intentional and visible.
Never use var in new code. There is no modern scenario where var is better than let or const. Every behavior that var provides can be replicated more safely with let and const.
Use let for counters, accumulators, and state that changes. Loops, running totals, and flags that toggle between states are legitimate uses for let.
Combine const with destructuring. Destructuring creates multiple const bindings in one statement, which is both readable and safe: const { name, age, role } = user;.
Enable ESLint rules for variable declarations. The no-var rule prevents var, and prefer-const suggests const wherever the variable is never reassigned. These rules enforce consistent usage across your codebase automatically.
Common Mistakes and How to Avoid Them
Variable Declaration Traps
These mistakes produce real bugs, especially when transitioning from var to modern JavaScript.
Using var in loops with callbacks. The classic closure bug where all callbacks share the same var variable. Always use let for loop variables.
Assuming const makes objects immutable. const prevents reassignment, not modification. Use Object.freeze() for shallow immutability, or libraries like Immer for deep immutability patterns.
Relying on var hoisting for "declaration at the top" patterns. Some developers intentionally use variables before their var declaration, relying on hoisting to initialize them as undefined. This is fragile and confusing. Declare variables before you use them.
Redeclaring var in the same function. Because var allows same-scope redeclaration, long functions can accidentally overwrite variables. let and const catch this as a SyntaxError.
Using let when const would work. If your linter is not enforcing prefer-const, you might default to let out of habit. Every let variable that is never reassigned should be const.
Next Steps
Learn why you should stop using var
Read our deep dive on why you should stop using var in JavaScript for a comprehensive look at every var behavior that causes real production bugs.
Understand JavaScript [data types](/tutorials/programming-languages/javascript/javascript-data-types-a-complete-beginner-guide)
Now that you know how to declare variables, learn what values they can hold: strings, numbers, booleans, objects, arrays, null, undefined, and more.
Explore functions and closures
Closures are directly related to scope. Understanding how JavaScript functions capture variables from outer scopes builds on the scope knowledge from this article.
Set up ESLint for your projects
Configure ESLint with no-var and prefer-const rules to automatically enforce best practices across your entire codebase.
Rune AI
Key Insights
- const by default: Use
constfor every variable; switch toletonly when you need reassignment - Block scope with let/const: Variables stay inside the
{}where they are declared, preventing accidental leaks - Temporal Dead Zone:
letandconstthrow errors if accessed before declaration, catching bugs thatvarsilently hides - var is legacy: Function scope, hoisting as
undefined, same-scope redeclaration, and globalwindowattachment are all sources of bugs - Loop safety: Always use
letfor loop counters;varcauses the classic closure bug where all async callbacks share the same value
Frequently Asked Questions
Should I ever use var in modern JavaScript?
Is const truly constant in JavaScript?
What is the Temporal Dead Zone?
Why was let not added from the beginning?
Can I mix var, let, and const in the same file?
Does let have any performance difference compared to var?
Conclusion
The choice between var, let, and const is not a matter of personal preference; it is a matter of code quality and bug prevention. var's function scope, hoisting-as-undefined behavior, and redeclaration allowance are all sources of real production bugs. let and const fix these issues with block scope, the temporal dead zone, and strict redeclaration rules. Defaulting to const and switching to let only when reassignment is needed produces the most readable, predictable, and maintainable JavaScript code.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.