Mateen Kiani
Published on Tue Jul 01 2025·5 min read
JavaScript variables are the building blocks of any script you write, but their behavior can change dramatically depending on how you declare them. Most developers learn about var, let, and const early on, yet the subtle differences in scope, hoisting, and mutability often cause confusion in larger projects. Have you ever wondered why using var sometimes leads to unexpected behavior compared to let or const?
The answer lies in understanding how each keyword handles scope, hoisting, and assignment. Grasping these differences helps you write clearer code, avoid sneaky bugs, and make informed decisions when choosing the right declaration for your needs.
Older JavaScript code uses var almost exclusively. However, var declarations are function-scoped rather than block-scoped, which can introduce unexpected leaks. For example:
function example() {if (true) {var x = 10;}console.log(x); // 10, even though x was inside the if block}
Here, var doesn’t respect the if block, so x is available throughout the function. In large functions, this can lead to accidental overwrites and hard-to-find bugs. Also, var allows redeclaration:
var name = 'Alice';var name = 'Bob';console.log(name); // Bob
Redeclaring a variable without an error can mask typos or logic mistakes. Despite these drawbacks, var still works in older environments. If you need to support legacy browsers without transpilation, var might be your only choice. But in modern code, avoid mixing var with let or const to keep behavior predictable.
Tip: If you see var in code, refactor to let or const when possible to leverage block scoping and clearer intent.
Introduced in ES6, let brings block scope to JavaScript. Variables declared with let only live inside the nearest enclosing {}
. This change greatly reduces accidental leaks:
for (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 100);}// Logs 0, 1, 2 as expected
With var, that same loop would log 3, 3, 3, because var is function-scoped. Another advantage of let is that it prevents redeclaration in the same scope:
let age = 25;let age = 30; // SyntaxError: Identifier 'age' has already been declared
This behavior catches mistakes early. Because let does not allow redeclaration, your code signals intent clearly: you plan to reassign the variable, but not create it twice.
However, let variables are mutable. You can change their value freely:
let count = 1;count = 2;
Use let when a value needs to change over time, such as counters, flags, or accumulators.
Const also offers block scope, but with a twist: the binding is read-only. Once you assign a value, you can’t reassign that variable:
const maxItems = 5;maxItems = 10; // TypeError: Assignment to constant variable.
That doesn’t mean the value itself is frozen. For objects and arrays, const prevents rebinding but not mutation:
const user = { name: 'Alice' };user.name = 'Bob'; // Alloweduser = {}; // Error
Use const by default. It shows your intent clearly: this variable won’t be reassigned. When you need mutability, switch to let. This practice leads to safer, more predictable code.
Tip: Default to const. Only pick let when you know the variable’s value must change.
All three declarations—var, let, and const—are hoisted, but they behave differently at runtime. Here’s how they compare:
Keyword | Hoisted to Top | Temporally Dead Zone | Default Value |
---|---|---|---|
var | Yes | No | undefined |
let | Yes | Yes | ReferenceError |
const | Yes | Yes | ReferenceError |
console.log(aVar); // undefinedconsole.log(aLet); // ReferenceErrorconsole.log(aConst); // ReferenceErrorvar aVar = 1;let aLet = 2;const aConst = 3;
The “Temporally Dead Zone” between the start of the scope and the declaration means let and const throw errors if accessed too early. This helps catch mistakes before they run.
Consider a form handler that tracks submission status:
function submitForm(data) {let status = 'pending';try {// send data...status = 'success';} catch (e) {status = 'error';}console.log(status);}
Here, let works well since status changes. If status were const, you’d get an error. On the other hand, configuration values can use const:
const API_ENDPOINT = 'https://api.example.com';
That binding should never change. Mixing var here could accidentally override your endpoint. Also, be mindful of global leaks: var declares variables on the global object when used at the top level, while let and const do not. To learn more about global scope pitfalls, check out are-javascript-variables-global.
• Default to const for all variables.
• Switch to let when you need to reassign.
• Avoid var entirely in modern code.
• Keep your scopes small—use blocks to contain logic.
• Name variables clearly to reflect mutability (e.g., finalCount for a const counter).
These guidelines help maintain consistency across your codebase and reduce time spent chasing scope-related bugs.
Understanding var, let, and const ensures that your JavaScript code behaves predictably and is easy to maintain. Var’s function scope can lead to accidental leaks, while let and const offer block-scope safety. Const signals unchanging bindings, and let allows controlled mutability. By defaulting to const, falling back to let when necessary, and avoiding var, you’ll write clearer, more reliable code. Keep these rules in mind in every project, and your future self—and your teammates—will thank you.