JavaScript Variable Declarations: Understanding var, let, and const
Modern JavaScript development demands clarity in JavaScript variable declarations—yet many developers still struggle with choosing between var, let, and const. Understanding ES6 variable scope isn’t just about memorizing rules; it’s about writing code that communicates intent and prevents bugs before they happen.
Key Takeaways
- Block scope (
let/const) provides better variable containment than function scope (var) - The Temporal Dead Zone prevents using variables before initialization
constprevents reassignment but not mutation of objects and arrays- Modern JavaScript favors
constby default,letwhen needed, and avoidsvar
Understanding JavaScript’s Three Declaration Keywords
JavaScript offers three ways to declare variables, each with distinct behaviors around scope, hoisting, and reassignment. Your choice signals different intentions to both the JavaScript engine and other developers reading your code.
Block Scope vs Function Scope
The fundamental difference between modern ES6 variable scope and legacy patterns lies in how variables are contained:
function processData() {
if (true) {
var x = 1; // Function-scoped
let y = 2; // Block-scoped
const z = 3; // Block-scoped
}
console.log(x); // 1 (accessible)
console.log(y); // ReferenceError
console.log(z); // ReferenceError
}
With var, variables leak out of blocks but remain within functions. Both let and const respect block boundaries—any curly braces create a new scope, including those in loops, conditionals, and plain blocks.
The Temporal Dead Zone Explained
JavaScript variable declarations with let and const are hoisted but remain uninitialized until their declaration is reached. This creates the Temporal Dead Zone (TDZ):
console.log(myVar); // undefined (hoisted, initialized)
console.log(myLet); // ReferenceError (TDZ)
console.log(myConst); // ReferenceError (TDZ)
var myVar = 1;
let myLet = 2;
const myConst = 3;
The TDZ prevents the subtle bugs that var’s behavior often caused. You can’t accidentally use a variable before it’s properly initialized—the engine throws an error immediately.
Reassignment vs Mutation
A critical distinction many developers miss: const prevents reassignment, not mutation:
const user = { name: 'Alice' };
user.name = 'Bob'; // Allowed (mutation)
user = { name: 'Charlie' }; // TypeError (reassignment)
const scores = [95, 87];
scores.push(91); // Allowed
scores = [100, 100]; // TypeError
For true immutability, combine const with Object.freeze() or immutable data libraries. The declaration keyword alone doesn’t make your data immutable—it makes the binding immutable.
Discover how at OpenReplay.com.
Modern Best Practices for Variable Declarations
JavaScript codebases today follow a clear hierarchy:
-
Default to
const: Use for values that won’t be reassigned. This includes most variables, even objects and arrays you’ll mutate. -
Use
letfor reassignment: Loop counters, accumulator variables, and values that genuinely need rebinding. -
Avoid
varin new code: There’s no modern use case wherevarprovides benefits overlet.
Modern tooling enforces these patterns. ESLint’s no-var rule and prefer-const rule catch violations automatically. TypeScript treats ES6 variable scope rules as fundamental, making block scope the default mental model.
Framework Considerations
React hooks depend on JavaScript variable declarations behaving predictably:
function Counter() {
const [count, setCount] = useState(0); // const for the binding
// Wrong: Can't reassign
// count = count + 1;
// Right: Use the setter
setCount(count + 1);
}
Vue’s Composition API and other modern frameworks follow similar patterns, where const dominates because reactivity systems handle updates through methods, not reassignment.
Why Legacy Code Still Uses var
You’ll encounter var in older codebases for historical reasons. Before ES6 (2015), it was the only option. Migration requires careful testing because:
- Changing
vartoletin loops can fix bugs or introduce them, depending on closure behavior - Some legacy code deliberately exploits
var’s function-scope hoisting - Older build tools might not transpile ES6 variable scope correctly
When refactoring, use automated tools like jscodeshift with thorough test coverage. Don’t manually convert thousands of declarations—let tooling handle the mechanical changes while you verify behavior.
Conclusion
JavaScript variable declarations in modern code follow a simple rule: use const by default, let when you need reassignment, and never var. This isn’t about following trends—it’s about writing code that clearly expresses intent and leverages ES6 variable scope to prevent entire categories of bugs. The Temporal Dead Zone, block scoping, and reassignment rules aren’t obstacles—they’re guardrails that guide you toward more maintainable JavaScript.
FAQs
Yes, const only prevents reassigning the variable itself, not modifying the contents. You can add, remove, or change properties in objects and elements in arrays declared with const. The variable always points to the same object or array in memory.
You'll get a ReferenceError due to the Temporal Dead Zone. Unlike var which returns undefined when accessed before declaration, let and const variables exist in an uninitialized state until the code reaches their declaration line.
Only with careful testing. While generally beneficial, changing var to let in loops can alter closure behavior. Use automated refactoring tools with comprehensive test coverage rather than manual changes to avoid introducing bugs.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.