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.

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

javascript
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 = {};                   // TypeError

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

javascript
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" - untouched

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

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

javascript
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 = {};                   // TypeError

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

javascript
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] - unchanged

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

javascript
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}

JavaScript const FAQ

What's the main difference between const and let?

Const cannot be reassigned after its initial declaration, and it must be initialized at the point of declaration. Let can be reassigned and doesn't require immediate initialization. Both are block-scoped. The practical advice: use const by default, switch to let only when you have a specific reason to reassign.

Can I modify objects and arrays declared with const?

Yes. Const only prevents reassigning the variable - that is, pointing it at a different object or array. You can freely add, modify, and delete properties on a const object. You can push, pop, splice, and otherwise mutate a const array. What throws a TypeError is writing something like user = {} or colors = [] when those are declared with const.

When should I use const vs let?

Use const for everything by default. The only time to switch to let is when you have a specific reason to reassign the variable - a counter that increments, a flag that flips, a variable that gets populated from an async operation. If you're not sure, start with const and change to let if the code requires reassignment.

Why does const require initialization?

Because the only moment you can give a const a value is at declaration - since reassignment is not allowed, there's no later opportunity. A const declared without a value would be permanently undefined with no way to fix it. The SyntaxError happens at parse time, before the code runs.

Can I use const in for loops?

In for...of and for...in loops, yes - each iteration creates a fresh binding, so const works correctly and gives you a per-iteration constant. In traditional for loops, no - the counter variable like i needs to be incremented, which is a reassignment, so use let for that. You can use const for any variables you create inside the loop body.

How can I make objects truly immutable with const?

Object.freeze prevents properties from being added, modified, or deleted. But freeze is shallow - it only affects the top level, so nested objects can still be mutated. For full deep immutability, you need to recursively freeze nested objects (a deepFreeze function is easy to write) or use a library like Immer which is common in React applications.

What happens if I try to reassign a const variable?

A TypeError is thrown at runtime: 'Assignment to constant variable'. This happens when the reassignment line executes - it's a runtime error, not a parse-time error like the missing initializer. In strict mode, attempts to modify frozen object properties also throw TypeErrors.

Is const hoisted like var?

Const is hoisted in the sense that the engine knows the variable exists from the start of the block. But unlike var, it's not initialized - it's in the Temporal Dead Zone until the declaration line is reached. Accessing it before that line throws a ReferenceError. This is different from var, which is hoisted and initialized to undefined, silently returning undefined when accessed early.