Why and how to use Object.freeze
to prevent mutations, what its limits are, and when to prefer alternatives like Object.seal
, immutability patterns, or TypeScript.

Object.freeze
to prevent mutations, what its limits are, and when to prefer alternatives like Object.seal
, immutability patterns, or TypeScript.Introduction
Have you ever debugged a bug that came from silent mutations?
const config = { retries: 3 };
config.retries = 99; // Oops, changed global config!
In big apps — React, Redux, Node services — mutations like this can wreak havoc.
Enter Object.freeze
: a built-in tool that makes objects immutable at runtime.
But here’s the catch: Object.freeze
isn’t a silver bullet. It has quirks, limits, and trade-offs. This post explains how it works, when to use it, and how to avoid its gotchas.
1. What Object.freeze
Does
const obj = { a: 1 };
Object.freeze(obj);
obj.a = 2; // ❌ silently ignored in non-strict mode
delete obj.a; // ❌ fails
obj.b = 3; // ❌ fails
console.log(obj); // { a: 1 }
- Properties cannot be added, removed, or reassigned.
- Property descriptors (
writable
,configurable
) are set tofalse
. - Works shallowly — nested objects aren’t frozen.
2. Freezing vs. Other Object Controls

👉 Use freeze
when you want read-only objects.
👉 Use seal
when you want to allow changes to existing values but no new keys.
3. Deep Freeze: Handling Nested Objects
Object.freeze
doesn’t recurse by default:
const settings = { ui: { theme: "dark" } };
Object.freeze(settings);
settings.ui.theme = "light"; // ✅ still works! (ui not frozen)
To fully lock it down, implement deepFreeze:
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach((prop) => {
const value = obj[prop];
if (value && typeof value === "object") {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const frozen = deepFreeze({ a: { b: 2 } });
frozen.a.b = 99; // ❌ fails
4. Strict Mode Behavior
In non-strict mode: writes fail silently.
In strict mode: they throw TypeError
.
"use strict";
const cfg = Object.freeze({ retries: 3 });
cfg.retries = 10; // ❌ TypeError: Cannot assign to read only property
👉 Strict mode makes bugs more visible — prefer it in production code.
5. Real-World Use Cases
A) Freezing Configs
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
RETRIES: 3,
});
Guarantees global constants aren’t mutated.
B) Redux/State Management
function reducer(state, action) {
Object.freeze(state); // dev safeguard
// ...process action immutably
}
Helps catch accidental mutations during development.
C) Freezing Constants/Enums
const ROLES = Object.freeze({
ADMIN: "admin",
USER: "user",
GUEST: "guest",
});
Safe way to store constant sets.
D) Freezing Shared Objects in APIs
function init(options) {
const safeOptions = Object.freeze(options);
return safeOptions;
}
Prevents external consumers from mutating shared configs.
6. TypeScript & Object.freeze
TypeScript has its own static immutability helpers, but you can combine them with runtime freeze
.
A) Readonly typing
const CONFIG = Object.freeze({
url: "https://api.example.com",
retries: 3,
});
// In TS: inferred as { readonly url: string; readonly retries: number }
B) Manual Readonly
type
type User = { id: number; name: string };
const u: Readonly<User> = { id: 1, name: "Ali" };
👉 Readonly<T>
gives compile-time safety; Object.freeze
gives runtime safety. Use both when you need belts-and-suspenders.
7. Alternatives to Freezing
- Immutable libraries: Immer, [Immutable.js] — better ergonomics for complex state.
- Linting rules: ESLint
no-param-reassign
helps prevent mutation. - TypeScript: Static
Readonly
types catch issues before runtime.
8. Gotchas & Limitations
- Shallow freeze: nested objects still mutable unless you deep freeze.
- Performance cost: Freezing adds overhead; avoid on hot paths or large objects.
- Not polyfillable: Works only in ES5+ environments.
- Array quirks: Frozen arrays can’t be modified, but still iterable.
const arr = Object.freeze([1,2,3]);
arr.push(4); // ❌ TypeError
5. Class instances: Methods still callable; only property mutability is blocked.
9. Handy Utility Functions
Deep freeze (safe export)
export const deepFreeze = (obj) => {
Object.getOwnPropertyNames(obj).forEach((prop) => {
if (
obj[prop] &&
typeof obj[prop] === "object" &&
!Object.isFrozen(obj[prop])
) {
deepFreeze(obj[prop]);
}
});
return Object.freeze(obj);
};
Immutable clone
Instead of freezing in place, clone + freeze:
const freezeClone = (obj) => deepFreeze(structuredClone(obj));
Conclusion
Object.freeze
is a powerful but simple guard against unwanted mutations.
- ✅ Great for configs, enums, shared constants, and dev-time safety in reducers.
- ⚠️ Remember it’s shallow — use
deepFreeze
for full immutability. - ⚡ Don’t overuse in hot paths; consider libraries like Immer for ergonomics.
- 🔒 Combine with TypeScript’s
Readonly
for compile + runtime safety.
Pro Tip: Treat Object.freeze
as a safety net, not your only tool. For app state, use immutability patterns or Immer; for constants/configs, freeze them once and forget about accidental bugs.
Call to Action (CTA)
Have you ever been bitten by a sneaky object mutation?
Share your story in the comments — and send this to a teammate who still mutates configs in place.
Leave a Reply