When to Use let vs const in Modern JavaScript
Learn when to use let vs const in JavaScript with practical examples, best practices, and real-world scenarios that help you write cleaner, more predictable code in every project.
Choosing between let and const is one of the first decisions you make in every JavaScript function, loop, and module. While var still works, modern JavaScript has moved on. The ECMAScript 2015 specification introduced let and const as block-scoped alternatives that eliminate entire categories of bugs. This guide breaks down exactly when to use each one, with practical patterns you will encounter in production codebases.
If you are new to JavaScript variables in general, the JavaScript variables tutorial covers declarations, naming conventions, and scope fundamentals. This article focuses specifically on the let vs const decision.
Understanding let and const
Both let and const are block-scoped, meaning they only exist inside the nearest pair of curly braces. The key difference is reassignment.
const creates a binding that cannot be reassigned after initialization. let creates a binding that can be reassigned at any time.
const apiUrl = "https://api.example.com/v2";
apiUrl = "https://api.example.com/v3"; // TypeError: Assignment to constant variable
let retryCount = 0;
retryCount = 1; // Works fine
retryCount = 2; // Still fineconst Does Not Mean Immutable
A common misconception is that const makes a value immutable. It does not. const prevents reassignment of the variable binding, but object properties and array elements can still change.
const config = {
theme: "dark",
language: "en",
};
config.theme = "light"; // This works. The object mutates.
config.language = "fr"; // This also works.
config = {}; // TypeError: Assignment to constant variableconst scores = [95, 87, 92];
scores.push(88); // Works. Array is now [95, 87, 92, 88]
scores[0] = 100; // Works. Array is now [100, 87, 92, 88]
scores = []; // TypeError: Assignment to constant variableThink of const as locking the label on a box, not locking the contents inside the box. The label stays on the same box, but you can rearrange what is inside.
The Default-to-const Rule
The most widely adopted convention in modern JavaScript is to default to const and only use let when you know the value needs to change. This is not just a style preference. It communicates intent to anyone reading your code.
| Declaration | Signal to Reader | Use When |
|---|---|---|
const | "This binding never changes" | Default for all values |
let | "This binding will be reassigned" | Counters, accumulators, state that mutates |
var | "This is legacy code" | Never in new code |
When another developer sees const, they immediately know the binding stays the same throughout the block. When they see let, they know to watch for reassignment later in the function. This reduces cognitive load during code review.
When to Use const
Use const for any value that does not need reassignment. In practice, this covers the majority of your declarations.
Configuration Values and Constants
const MAX_RETRIES = 3;
const API_TIMEOUT_MS = 5000;
const BASE_URL = "https://api.runehub.dev";
const userRoles = Object.freeze({
ADMIN: "admin",
EDITOR: "editor",
VIEWER: "viewer",
});Using Object.freeze() with const creates a truly immutable object where neither the binding nor the properties can change. Without freeze, only the binding is locked.
Function Declarations
Arrow functions assigned to variables should always use const because you never want to accidentally reassign a function.
const calculateTax = (price, rate) => {
return price * rate;
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};DOM References and API Responses
const submitButton = document.querySelector("#submit-btn");
const formFields = document.querySelectorAll(".form-field");
const response = await fetch(`${BASE_URL}/users`);
const data = await response.json();Even though response and data might seem like they could change, each fetch call creates a new scope. Within that scope, the binding stays the same.
Destructured Values
const { name, email, role } = user;
const [first, second, ...rest] = sortedScores;Destructured bindings almost always use const because they represent extracted snapshots of data at a specific point in time.
When to Use let
Use let only when you know the variable will be reassigned. Here are the common patterns.
Loop Counters
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
let attempts = 0;
while (attempts < MAX_RETRIES) {
const result = await tryConnection();
if (result.success) break;
attempts++;
}The i counter and attempts both get reassigned on each iteration, so let is the correct choice.
Accumulators and Running Totals
let totalPrice = 0;
let itemCount = 0;
for (const item of cart) {
totalPrice += item.price * item.quantity;
itemCount += item.quantity;
}
console.log(`${itemCount} items, total: $${totalPrice.toFixed(2)}`);Notice how item uses const inside the for...of loop. Each iteration creates a new binding for item, so const is correct there. But totalPrice and itemCount accumulate across iterations, requiring let.
Conditional Assignment
let discountRate;
if (user.isPremium) {
discountRate = 0.2;
} else if (user.orderCount > 10) {
discountRate = 0.1;
} else {
discountRate = 0;
}When a variable's value depends on multiple conditions and gets assigned inside different branches, let is necessary because the declaration and assignment happen in separate statements.
Refactoring Tip
You can often eliminate let by restructuring conditional assignments into a single expression using a ternary or a lookup object. For example: const discountRate = user.isPremium ? 0.2 : user.orderCount > 10 ? 0.1 : 0;
Swap Operations
let a = 10;
let b = 20;
// Classic swap
let temp = a;
a = b;
b = temp;
// Modern swap with destructuring
[a, b] = [b, a];Real-World Decision Framework
Here is a practical workflow for deciding between let and const in any situation:
| Question | If Yes | If No |
|---|---|---|
| Will this binding ever point to a different value? | Use let | Use const |
| Is this a loop counter or accumulator? | Use let | Use const |
| Is this assigned inside conditional branches? | Use let (or refactor) | Use const |
| Am I unsure? | Start with const | Start with const |
The rule is simple: start with const. Your linter or runtime will tell you if you need let.
Best Practices
Production Guidelines
These practices are used in major open-source projects like React, Next.js, and the Node.js runtime itself.
Always start with const. Write every declaration as const first. Only change to let when the code requires reassignment. This makes your intent clear and catches accidental mutations. Most linters (ESLint's prefer-const rule) enforce this automatically.
One declaration per line. Avoid chaining declarations with commas (const a = 1, b = 2, c = 3). Each declaration on its own line is easier to read, diff in version control, and reorder.
Declare variables close to first use. Do not hoist all declarations to the top of a function like older JavaScript conventions suggested. Declare each variable in the smallest scope where it is needed, right before its first use.
Use Object.freeze for true constants. When you need an object or array that genuinely should not change, combine const with Object.freeze(). For deep nesting, use a library like immer or a recursive freeze utility.
Name const values descriptively. Since const values provide stable reference points, give them clear names. const maxRetries = 3 communicates more than const n = 3.
Common Mistakes and How to Avoid Them
Watch Out for These Pitfalls
These mistakes appear frequently in code reviews and can introduce subtle bugs.
Assuming const makes objects immutable. As shown earlier, const only prevents reassignment of the binding. Object properties and array elements can still be modified. If you need full immutability, use Object.freeze() or a library like Immer.
Using let "just in case" the value might change. This is the most common bad habit. Defaulting to let tells every reader "this value will change later," which becomes a false signal when it never does. Start with const and downgrade to let only when necessary.
Forgetting about the Temporal Dead Zone (TDZ). Both let and const are hoisted but not initialized. Accessing them before their declaration line throws a ReferenceError, unlike var which returns undefined.
console.log(name); // ReferenceError: Cannot access 'name' before initialization
const name = "RuneHub";Using var in loops and getting unexpected closures. With var, loop variables share a single binding across all iterations. With let, each iteration gets its own binding, which is almost always what you want.
// Bug with var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Prints: 3, 3, 3
}
// Fixed with let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Prints: 0, 1, 2
}Declaring variables in a scope wider than necessary. If a variable is only used inside an if block, declare it there, not at the function level.
Next Steps
Learn JavaScript data types
Now that you understand variable declarations, explore the different data types JavaScript offers and how each behaves with let and const.
Practice with control flow
Apply your let and const knowledge to loops and conditionals using real interactive examples.
Configure ESLint prefer-const
Set up the prefer-const ESLint rule in your project to automatically catch places where let should be const. This enforces the pattern across your entire team.
Explore scope and hoisting deeper
Understanding how block scope, function scope, and the Temporal Dead Zone interact will solidify your grasp of modern variable declarations.
Rune AI
Key Insights
- Default to const: use
constfor every declaration unless you specifically need reassignment - const does not mean immutable: object properties and array elements can still change; use
Object.freeze()for true immutability - Use let for counters and accumulators: loop variables, running totals, and conditional assignments are the primary use cases for
let - Never use var in new code:
letandconstare block-scoped and avoid hoisting bugs that plaguevar - Start with const, downgrade if needed: your linter will catch any
constthat should belet
Frequently Asked Questions
Can I change the properties of a const object in JavaScript?
Should I ever use var in modern JavaScript?
Does using const improve JavaScript performance?
What happens if I try to reassign a const variable?
How do let and const behave inside loops?
Conclusion
Choosing between let and const is a small decision with a big impact on code readability and maintainability. The modern JavaScript convention is clear: default to const for every declaration and switch to let only when reassignment is genuinely required. This pattern, enforced by ESLint's prefer-const rule, communicates your intent to every developer who reads your code and eliminates accidental rebinding bugs.
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.