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.

JavaScriptbeginner
10 min read

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.

javascriptjavascript
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 fine

const 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.

javascriptjavascript
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 variable
javascriptjavascript
const 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 variable

Think 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.

DeclarationSignal to ReaderUse 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

javascriptjavascript
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.

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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:

QuestionIf YesIf No
Will this binding ever point to a different value?Use letUse const
Is this a loop counter or accumulator?Use letUse const
Is this assigned inside conditional branches?Use let (or refactor)Use const
Am I unsure?Start with constStart 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.

javascriptjavascript
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.

javascriptjavascript
// 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

Rune AI

Key Insights

  • Default to const: use const for 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: let and const are block-scoped and avoid hoisting bugs that plague var
  • Start with const, downgrade if needed: your linter will catch any const that should be let
RunePowered by Rune AI

Frequently Asked Questions

Can I change the properties of a const object in JavaScript?

Yes. The `const` keyword prevents reassignment of the variable binding, not mutation of the value it holds. You can freely add, modify, or delete properties on a `const` object. To prevent property changes, combine `const` with `Object.freeze()`.

Should I ever use var in modern JavaScript?

No. Both `let` and `const` are block-scoped and avoid the hoisting pitfalls of `var`. The only reason to encounter `var` is in legacy codebases or when targeting extremely old [JavaScript engine](/tutorials/programming-languages/javascript/what-is-a-javascript-engine-a-complete-guide)s that do not support ES2015.

Does using const improve JavaScript performance?

In most JavaScript engines, `const` and `let` perform identically. The V8 engine (used in Chrome and Node.js) does not currently optimize `const` differently from `let` in terms of execution speed. The benefit of `const` is readability and intent signaling, not raw performance.

What happens if I try to reassign a const variable?

JavaScript throws a `TypeError: Assignment to constant variable` at the line where the reassignment is attempted. In [strict mode](/tutorials/programming-languages/javascript/javascript-strict-mode-use-strict-explained), this error is thrown at runtime. This makes bugs easier to catch compared to `var`, where accidental overwrites go unnoticed.

How do let and const behave inside loops?

In a `for` loop, `let` creates a new binding for each iteration, so closures inside the loop capture the correct value. `const` can be used in `for...of` and `for...in` loops because each iteration creates a fresh binding. However, `const` cannot be used as a traditional `for` loop counter because the counter needs reassignment (`i++`).

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.