JavaScript const Keyword Complete Guide
Understanding JavaScript const
Const was added in ES6 alongside let to replace var, and it's the one you should reach for first in almost every situation. The rule is simple: use const by default, switch to let only when you have a specific reason to reassign the variable. The two properties that distinguish const from let are that it cannot be reassigned after declaration, and it must be initialized at the point of declaration. Like let, it's block-scoped and has the Temporal Dead Zone behavior. The common misconception is that const makes values immutable - it doesn't. It makes the variable binding immutable, which is a different thing.
Key Characteristics of const
Block scoped- Only accessible within the block it's declared in - same as let.Cannot be reassigned- Once initialized, the variable cannot be pointed at a different value. Attempting reassignment throws TypeError.Must be initialized- You have to give it a value at declaration. const x; without a value is a SyntaxError.Temporal Dead Zone- Hoisted but not initialized - accessing before its declaration line throws ReferenceError.
Basic const Declaration
For primitive values, const behaves exactly like you'd expect from 'constant': the value cannot change. Number, string, boolean - once set, that's it. Attempting reassignment throws a TypeError at runtime, and attempting to declare without initializing throws a SyntaxError before the code even runs.
1// Basic declarations
2const age = 25;
3const name = "John Doe";
4const isActive = true;
5const PI = 3.14159;
6
7// Reassignment throws TypeError
8const MAX_USERS = 100;
9// MAX_USERS = 200; // TypeError: Assignment to constant variable
10// MAX_USERS++; // TypeError: same reason
11
12// Initialization is required at declaration
13const API_KEY = "abc123def456"; // Fine
14// const UNINITIALIZED; // SyntaxError: Missing initializer in const declaration
15
16// Constants in computations
17const TAX_RATE = 0.07;
18const price = 100;
19const total = price * (1 + TAX_RATE);
20console.log(total); // 107
21
22// UPPER_SNAKE_CASE convention for values that are truly constant
23const MAX_FILE_SIZE = 10485760;
24const DEFAULT_TIMEOUT = 5000;
25const API_BASE_URL = "https://api.example.com";Declaration Rules
Initialization required- Must assign a value during declaration. This is enforced at parse time - a SyntaxError before the code runs.No reassignment- Cannot change the value after initialization. This is enforced at runtime - a TypeError when the reassignment is reached.Block scope- Only accessible within the block where it's declared.UPPER_SNAKE_CASE convention- Typically used for module-level constants. camelCase is also fine for local constants inside functions.
const with Objects and Arrays
This is where the misconception lives. Const prevents reassigning the variable - it does not prevent modifying the object or array the variable points to. So you can add, change, and delete properties on a const object. You can push, pop, splice, and mutate a const array. What you cannot do is replace the whole thing with person = {} or colors = []. The variable is constant; the value it holds is not.
1// const objects: properties can change, binding cannot
2const user = {
3 name: "Alice",
4 age: 30,
5 email: "alice@example.com"
6};
7
8user.age = 31; // Fine
9user.city = "New York"; // Fine - adding a property
10delete user.email; // Fine - removing a property
11
12console.log(user); // { name: "Alice", age: 31, city: "New York" }
13
14// user = { name: "Bob" }; // TypeError: Assignment to constant variable
15
16// const arrays: elements can change, binding cannot
17const colors = ["red", "green", "blue"];
18
19colors[0] = "orange"; // Fine
20colors.push("yellow"); // Fine
21colors.pop(); // Fine
22colors.splice(1, 1, "purple"); // Fine
23
24// colors = ["pink", "cyan"]; // TypeError
25
26// Nested objects: same rules apply at every level
27const config = {
28 api: {
29 baseUrl: "https://api.example.com",
30 timeout: 5000
31 },
32 features: ["auth", "upload"]
33};
34
35config.api.timeout = 10000; // Fine - modifying nested property
36config.features.push("search"); // Fine
37
38// config = {}; // TypeErrorObject and Array Behavior
Binding is immutable- The variable cannot be reassigned to a different object or array.Contents are mutable- Properties can be added, changed, and deleted. Array elements can be modified.Reference stays fixed- The variable always points to the same object or array in memory.Nested objects- Same rules apply at every level - you can modify nested properties and arrays.
Block Scoping with const
Const is block-scoped - it only exists within the curly braces it was declared in. Declaring a const inside an if block, a for loop, or any other block creates a variable that doesn't exist outside. Each block can also have its own const with the same name without conflict - they're different variables that happen to have the same name in different scopes.
1// Block scope: const doesn't leak out of blocks
2function demonstrateBlockScope() {
3 const functionConst = "accessible in entire function";
4
5 if (true) {
6 const blockScoped = "only in this if block";
7 console.log(blockScoped); // Works
8 console.log(functionConst); // Works
9 }
10
11 console.log(functionConst); // Works
12 // console.log(blockScoped); // ReferenceError
13}
14
15// Same name in different scopes - three separate variables
16const globalConst = "I'm global";
17
18function testScopes() {
19 const globalConst = "I'm shadowing the global";
20 console.log(globalConst); // "I'm shadowing the global"
21
22 if (true) {
23 const globalConst = "I'm in a block";
24 console.log(globalConst); // "I'm in a block"
25 }
26
27 console.log(globalConst); // "I'm shadowing the global"
28}
29
30testScopes();
31console.log(globalConst); // "I'm global" - untouchedconst in Loops and Iteration
Const works fine in for...of and for...in loops - each iteration gets a fresh binding, so the constraint that const can't be reassigned is satisfied because it's a new const each time. Traditional for loops don't work with const for the counter variable because the loop increments i++, which is a reassignment. Use let for loop counters. Inside the loop body, using const for values derived within that iteration is perfectly correct.
1// for...of: new const binding per iteration
2const fruits = ["apple", "banana", "orange"];
3
4for (const fruit of fruits) {
5 console.log(fruit); // "apple", "banana", "orange"
6 // fruit = "grape"; // TypeError - can't reassign within iteration
7}
8
9// for...in: same - new binding per key
10const person = { name: "John", age: 30, city: "Boston" };
11
12for (const key in person) {
13 console.log(`${key}: ${person[key]}`);
14}
15
16// Traditional for loop: counter needs let, inner values can use const
17// for (const i = 0; i < 5; i++) {} // TypeError - i++ is reassignment
18
19for (let i = 0; i < 3; i++) {
20 const message = `Iteration ${i}`; // New const each iteration, this is fine
21 console.log(message);
22}
23
24// Array methods with const inside callbacks
25const numbers = [1, 2, 3, 4, 5];
26
27numbers.forEach((number) => {
28 const squared = number * number; // New const per call
29 console.log(squared); // 1, 4, 9, 16, 25
30});
31
32const doubled = numbers.map((number) => number * 2);
33console.log(doubled); // [2, 4, 6, 8, 10]const with Functions and Modules
Storing functions in const variables is standard modern JavaScript. The intent is clear: this is a function that will not be replaced. It's also slightly different from function declarations in that it's not hoisted - you can't call a const arrow function before the line where it's defined. For module exports, const values exported from a module are read-only from the perspective of the importing code.
1// Function expressions as const
2const calculateArea = function(radius) {
3 const PI = 3.14159;
4 return PI * radius * radius;
5};
6
7console.log(calculateArea(5)); // 78.53975
8// calculateArea = function() {}; // TypeError
9
10// Arrow functions as const
11const multiply = (a, b) => a * b;
12const greet = (name) => `Hello, ${name}!`;
13
14console.log(multiply(4, 5)); // 20
15console.log(greet("Alice")); // "Hello, Alice!"
16
17// Module-style objects
18const MathUtils = {
19 PI: 3.14159,
20
21 circleArea(radius) {
22 return this.PI * radius * radius;
23 },
24
25 circleCircumference(radius) {
26 return 2 * this.PI * radius;
27 }
28};
29
30console.log(MathUtils.circleArea(10)); // 314.159
31// MathUtils = {}; // TypeError - can't reassign
32MathUtils.PI = 3; // Fine - can modify properties
33
34// Configuration object
35const APP_CONFIG = {
36 version: "1.0.0",
37 environment: "production",
38 features: {
39 auth: true,
40 payments: false
41 }
42};
43
44APP_CONFIG.features.payments = true; // Fine
45// APP_CONFIG = {}; // TypeErrorImmutability Patterns with const
When you actually need the object to be immutable - not just the variable binding - Object.freeze is the built-in tool. It prevents adding, deleting, and modifying properties on the frozen object. The catch: freeze is shallow. Properties that are themselves objects can still be mutated. For full deep immutability you need to recursively freeze or use a library. The spread operator pattern for creating new objects and arrays rather than mutating existing ones is another common approach, especially in React applications where immutable updates are the standard pattern.
1// Object.freeze: shallow immutability
2const frozenConfig = Object.freeze({
3 name: "John",
4 age: 30,
5 address: {
6 city: "Boston"
7 }
8});
9
10// frozenConfig.age = 31; // Silently fails (throws in strict mode)
11// frozenConfig.newProp = "value"; // Silently fails (throws in strict mode)
12
13// Freeze is shallow - nested objects are not frozen
14frozenConfig.address.city = "Chicago"; // This still works!
15console.log(frozenConfig.address.city); // "Chicago"
16
17// Deep freeze for full immutability
18function deepFreeze(obj) {
19 Object.freeze(obj);
20 Object.getOwnPropertyNames(obj).forEach(prop => {
21 if (obj[prop] !== null &&
22 typeof obj[prop] === 'object' &&
23 !Object.isFrozen(obj[prop])) {
24 deepFreeze(obj[prop]);
25 }
26 });
27 return obj;
28}
29
30const deeplyFrozen = deepFreeze({
31 user: { profile: { name: "Alice", settings: { theme: "dark" } } }
32});
33// deeplyFrozen.user.profile.settings.theme = "light"; // Throws in strict mode
34
35// Immutable update pattern: create new, don't mutate
36const originalUser = { name: "John", age: 30 };
37
38const updatedUser = { ...originalUser, age: 31 }; // New object
39const withEmail = { ...originalUser, email: "j@ex.com" }; // New object
40
41console.log(originalUser); // { name: "John", age: 30 } - unchanged
42console.log(updatedUser); // { name: "John", age: 31 }
43
44// Same for arrays
45const original = [1, 2, 3];
46const withFour = [...original, 4]; // [1, 2, 3, 4]
47const doubled = original.map(x => x * 2); // [2, 4, 6]
48
49console.log(original); // [1, 2, 3] - unchangedconst Best Practices
The single most impactful const habit: use it by default. When you write let, you're communicating that this value will change somewhere in the code. When you write const, you're communicating it won't. That signal is useful for anyone reading the code, including yourself later. Descriptive names help too - TAX_RATE communicates more than tr, and DEFAULT_TIMEOUT_MS communicates more than timeout. Grouping related constants into objects keeps them organized and reduces the number of names you're putting in scope.
1// const by default, let only for reassignables
2const DEFAULT_TIMEOUT = 5000;
3const API_BASE_URL = "https://api.example.com";
4const MAX_RETRY_ATTEMPTS = 3;
5
6let currentUser = null; // will be reassigned
7let isLoading = false; // will change
8let requestCount = 0; // will increment
9
10// Group related constants
11const API_CONFIG = {
12 BASE_URL: "https://api.example.com",
13 TIMEOUT: 5000,
14 VERSION: "v1"
15};
16
17const ERROR_MESSAGES = {
18 NETWORK_ERROR: "Network connection failed",
19 AUTH_ERROR: "Authentication required",
20 VALIDATION_ERROR: "Invalid input data"
21};
22
23// const for function expressions communicates the function won't be replaced
24const calculateTotal = (items) => {
25 const TAX = 0.07;
26 const subtotal = items.reduce((sum, item) => sum + item.price, 0);
27 return subtotal * (1 + TAX);
28};
29
30// const with destructuring - common and readable
31const [first, second, ...rest] = [1, 2, 3, 4, 5];
32const { name, age, ...otherProps } = { name: "John", age: 30, city: "Boston" };
33
34// Declare const close to where it's used
35function processOrder(order) {
36 const TAX_RATE = 0.07; // near its use, not at the top of a large file
37 const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
38 return subtotal * (1 + TAX_RATE);
39}