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)
ParameterDescription
objThe object to freeze
ReturnsThe 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:

constObject.freeze()
What it protectsThe binding (variable name)The object contents
Prevents reassignment?YesNo (does not affect bindings)
Prevents property mutation?NoYes
ScopeVariable declarationObject 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

FeaturefreezesealpreventExtensions
Add propertiesNoNoNo
Delete propertiesNoNoYes
Modify valuesNoYesYes
Reconfigure descriptorsNoNoYes
Check methodisFrozen()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

  1. Object.freeze(obj) prevents adding, deleting, and modifying properties.
  2. It returns the same object (not a copy).
  3. It is a shallow freeze — nested objects must be frozen recursively with a deep-freeze helper.
  4. In strict mode, mutations throw TypeError; in sloppy mode they silently fail.
  5. const prevents variable reassignment; freeze prevents property mutation — use both together.
  6. Common use cases: config objects, enum-like constants, immutable state patterns.

Explain-It Challenge

Explain without notes:

  1. If you Object.freeze an object with a nested array, can you .push() to that array? Why?
  2. What is the difference between const obj = {} and Object.freeze(obj) in terms of what is protected?
  3. Write a one-line check: is a given object frozen?

Navigation: <- 1.24.b — Object.assign . 1.24.d — Object.fromEntries ->