Episode 1 — Fundamentals / 1.24 — Object Methods
1.24.c — Object.freeze()
In one sentence:
Object.freeze(obj)makes an object completely immutable at the top level — you cannot add, delete, or modify any of its properties.
Navigation: <- 1.24.b — Object.assign . 1.24.d — Object.fromEntries ->
1. What does Object.freeze() do?
Object.freeze() locks down an object so that:
- No properties can be added
- No properties can be deleted
- No property values can be changed
- No property descriptors can be reconfigured
const config = { host: "localhost", port: 3000 };
Object.freeze(config);
config.port = 8080; // silently fails (or throws in strict mode)
config.newProp = "hello"; // silently fails
delete config.host; // silently fails
console.log(config); // { host: "localhost", port: 3000 } -- unchanged
2. Syntax and return value
Object.freeze(obj)
| Parameter | Description |
|---|---|
obj | The object to freeze |
| Returns | The same object (not a copy) |
const obj = { a: 1 };
const frozen = Object.freeze(obj);
console.log(frozen === obj); // true -- same reference
3. Strict mode vs non-strict mode
In strict mode, any attempt to modify a frozen object throws a TypeError. In non-strict (sloppy) mode, modifications silently fail.
"use strict";
const settings = Object.freeze({ debug: false });
settings.debug = true;
// TypeError: Cannot assign to read only property 'debug' of object '#<Object>'
// Non-strict mode
const settings = Object.freeze({ debug: false });
settings.debug = true; // no error, just silently ignored
console.log(settings.debug); // false
Best practice: Always use strict mode (or modules, which are strict by default) so mutations fail loudly.
4. Checking if an object is frozen
Object.isFrozen(obj)
const obj = { a: 1 };
console.log(Object.isFrozen(obj)); // false
Object.freeze(obj);
console.log(Object.isFrozen(obj)); // true
Special cases:
// Empty non-extensible object is considered frozen
const empty = Object.preventExtensions({});
console.log(Object.isFrozen(empty)); // true
// Primitives are always "frozen"
console.log(Object.isFrozen(42)); // true
console.log(Object.isFrozen("hello")); // true
5. Shallow freeze only
Critical gotcha: Object.freeze() only freezes the top level. Nested objects remain fully mutable.
const app = Object.freeze({
name: "MyApp",
database: {
host: "localhost",
port: 5432,
},
});
app.name = "Other"; // blocked
app.database.port = 9999; // ALLOWED -- nested object is NOT frozen!
console.log(app.database.port); // 9999
This happens because freeze makes the reference to database immutable (you cannot reassign app.database to a different object), but it does not recursively freeze the object that database points to.
6. Deep freeze pattern
To truly make an entire object tree immutable, recursively freeze all nested objects:
function deepFreeze(obj) {
// Freeze the object itself
Object.freeze(obj);
// Recursively freeze all own property values that are objects
for (const value of Object.values(obj)) {
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
deepFreeze(value);
}
}
return obj;
}
const config = deepFreeze({
app: "MyApp",
database: {
host: "localhost",
port: 5432,
options: { ssl: true, timeout: 3000 },
},
});
config.database.port = 9999; // blocked!
config.database.options.ssl = false; // blocked!
console.log(config.database.port); // 5432
console.log(config.database.options.ssl); // true
Warning: Deep freeze can be expensive on large object trees. It also cannot handle circular references without additional tracking.
Deep freeze with circular reference protection
function deepFreeze(obj, visited = new WeakSet()) {
if (visited.has(obj)) return obj;
visited.add(obj);
Object.freeze(obj);
for (const value of Object.values(obj)) {
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
deepFreeze(value, visited);
}
}
return obj;
}
7. freeze vs const
This is a common source of confusion:
const | Object.freeze() | |
|---|---|---|
| What it protects | The binding (variable name) | The object contents |
| Prevents reassignment? | Yes | No (does not affect bindings) |
| Prevents property mutation? | No | Yes |
| Scope | Variable declaration | Object itself |
// const prevents reassignment
const obj = { a: 1 };
// obj = { a: 2 }; // TypeError: Assignment to constant variable
// but const does NOT prevent mutation
obj.a = 999;
console.log(obj.a); // 999
// freeze prevents mutation
const frozen = Object.freeze({ a: 1 });
frozen.a = 999;
console.log(frozen.a); // 1 -- unchanged
You almost always want both together:
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
TIMEOUT: 5000,
});
// Cannot reassign CONFIG (const) AND cannot modify its properties (freeze)
8. What exactly gets frozen?
When you freeze an object, every property becomes:
- Non-writable (value cannot change)
- Non-configurable (descriptor cannot be modified, property cannot be deleted)
- The object becomes non-extensible (no new properties)
const obj = { x: 10 };
Object.freeze(obj);
const desc = Object.getOwnPropertyDescriptor(obj, "x");
console.log(desc);
// {
// value: 10,
// writable: false, <-- was true
// enumerable: true,
// configurable: false <-- was true
// }
console.log(Object.isExtensible(obj)); // false
9. Freezing arrays
Arrays are objects, so Object.freeze() works on them too:
const colors = Object.freeze(["red", "green", "blue"]);
colors[0] = "yellow"; // silently fails
colors.push("purple"); // TypeError: Cannot add property 3, object is not extensible
colors.pop(); // TypeError
console.log(colors); // ["red", "green", "blue"]
10. Use cases
Immutable configuration
const CONFIG = Object.freeze({
API_BASE: "https://api.example.com/v2",
MAX_RETRIES: 3,
TIMEOUT_MS: 10000,
});
// Any accidental mutation is caught in strict mode
Enum-like objects
const STATUS = Object.freeze({
PENDING: "pending",
ACTIVE: "active",
INACTIVE: "inactive",
DELETED: "deleted",
});
function setStatus(user, status) {
if (!Object.values(STATUS).includes(status)) {
throw new Error(`Invalid status: ${status}`);
}
user.status = status;
}
setStatus(user, STATUS.ACTIVE);
Preventing accidental state mutation
function processData(rawData) {
// Freeze input to ensure this function does not mutate it
const data = Object.freeze(rawData);
// Any accidental mutation throws in strict mode
// data.processed = true; // TypeError!
// Return new object instead
return { ...data, processed: true };
}
Redux-style immutable state
function reducer(state = Object.freeze({ count: 0 }), action) {
switch (action.type) {
case "INCREMENT":
return Object.freeze({ ...state, count: state.count + 1 });
case "DECREMENT":
return Object.freeze({ ...state, count: state.count - 1 });
default:
return state;
}
}
11. Freeze vs Seal vs preventExtensions
| Feature | freeze | seal | preventExtensions |
|---|---|---|---|
| Add properties | No | No | No |
| Delete properties | No | No | Yes |
| Modify values | No | Yes | Yes |
| Reconfigure descriptors | No | No | Yes |
| Check method | isFrozen() | isSealed() | isExtensible() |
freeze is the strictest — it implies seal, which implies preventExtensions.
12. Edge cases
// Freezing null/undefined throws TypeError
// Object.freeze(null); // TypeError
// Object.freeze(undefined); // TypeError
// Freezing primitives returns them unchanged
Object.freeze(42); // 42
Object.freeze("str"); // "str"
// Freezing an already frozen object is a no-op (no error)
const obj = Object.freeze({ a: 1 });
Object.freeze(obj); // fine, returns obj
// Frozen objects can still be spread into new objects
const frozen = Object.freeze({ a: 1, b: 2 });
const extended = { ...frozen, c: 3 }; // { a: 1, b: 2, c: 3 } -- new, unfrozen object
Key takeaways
Object.freeze(obj)prevents adding, deleting, and modifying properties.- It returns the same object (not a copy).
- It is a shallow freeze — nested objects must be frozen recursively with a deep-freeze helper.
- In strict mode, mutations throw TypeError; in sloppy mode they silently fail.
constprevents variable reassignment;freezeprevents property mutation — use both together.- Common use cases: config objects, enum-like constants, immutable state patterns.
Explain-It Challenge
Explain without notes:
- If you
Object.freezean object with a nested array, can you.push()to that array? Why? - What is the difference between
const obj = {}andObject.freeze(obj)in terms of what is protected? - Write a one-line check: is a given object frozen?
Navigation: <- 1.24.b — Object.assign . 1.24.d — Object.fromEntries ->