JavaScript Object.freeze vs Object.seal

JavaScript objects are mutable by default. You can add properties, delete them, and modify values at any time. This flexibility is powerful but can lead to bugs when objects are unintentionally...

Key Insights

  • Object.seal() prevents adding or removing properties but allows modifying existing values, while Object.freeze() prevents all modifications including value changes
  • Both methods are shallow—nested objects remain mutable unless you implement recursive freezing
  • Use Object.freeze() for true constants and configuration objects; use Object.seal() when you need a fixed structure but mutable values

Introduction

JavaScript objects are mutable by default. You can add properties, delete them, and modify values at any time. This flexibility is powerful but can lead to bugs when objects are unintentionally modified, especially in large codebases where multiple parts of your application might reference the same object.

Object.seal() and Object.freeze() are built-in methods that restrict object mutability in different ways. They’re essential tools for defensive programming, creating immutable configuration objects, and preventing accidental modifications that could break your application. Understanding the differences between these methods will help you choose the right level of protection for your use case.

Understanding Object.seal()

Object.seal() prevents you from adding new properties to an object or deleting existing ones. However, it still allows you to modify the values of existing properties. Think of it as locking the structure of your object while keeping the contents flexible.

When you seal an object, JavaScript sets the object’s [[Extensible]] internal property to false and marks all existing properties as non-configurable. This means the property descriptors can’t be changed, but if a property is writable, its value can still be modified.

const user = {
  name: 'Alice',
  age: 30,
  role: 'developer'
};

Object.seal(user);

// Check if object is sealed
console.log(Object.isSealed(user)); // true

// Attempting to add a new property fails silently (or throws in strict mode)
user.email = 'alice@example.com';
console.log(user.email); // undefined

// Attempting to delete a property fails
delete user.role;
console.log(user.role); // 'developer' - still there

// Modifying existing properties works fine
user.age = 31;
console.log(user.age); // 31

user.name = 'Alice Smith';
console.log(user.name); // 'Alice Smith'

In non-strict mode, attempts to add or delete properties fail silently. In strict mode, these operations throw a TypeError. You can verify whether an object is sealed using Object.isSealed().

Understanding Object.freeze()

Object.freeze() is more restrictive than Object.seal(). It prevents all modifications: you can’t add properties, delete properties, or change existing property values. A frozen object is essentially immutable at the top level.

When you freeze an object, JavaScript makes it non-extensible (like seal) and marks all properties as non-configurable and non-writable. This creates a truly read-only object.

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

Object.freeze(config);

// Check if object is frozen
console.log(Object.isFrozen(config)); // true

// Attempting to add a new property fails
config.maxConnections = 10;
console.log(config.maxConnections); // undefined

// Attempting to delete a property fails
delete config.retries;
console.log(config.retries); // 3 - still there

// Attempting to modify existing properties fails
config.timeout = 10000;
console.log(config.timeout); // 5000 - unchanged

config.apiUrl = 'https://api.malicious.com';
console.log(config.apiUrl); // 'https://api.example.com' - unchanged

Like seal, freeze fails silently in non-strict mode and throws errors in strict mode. Use Object.isFrozen() to check if an object is frozen. Note that a frozen object is also sealed, so Object.isSealed() will return true for frozen objects.

Key Differences & Comparison Table

The fundamental difference is that sealed objects allow property value modifications while frozen objects don’t. Here’s a comprehensive comparison:

Operation Object.seal() Object.freeze()
Add new properties ❌ No ❌ No
Delete properties ❌ No ❌ No
Modify property values ✅ Yes ❌ No
Modify property descriptors ❌ No ❌ No
Object is extensible ❌ No ❌ No
Properties are configurable ❌ No ❌ No
Properties are writable ✅ Yes ❌ No

Let’s see these differences in action with a direct comparison:

const sealedObj = { count: 0, name: 'sealed' };
const frozenObj = { count: 0, name: 'frozen' };

Object.seal(sealedObj);
Object.freeze(frozenObj);

// Try to add properties
sealedObj.newProp = 'test';
frozenObj.newProp = 'test';
console.log(sealedObj.newProp); // undefined
console.log(frozenObj.newProp); // undefined

// Try to delete properties
delete sealedObj.name;
delete frozenObj.name;
console.log(sealedObj.name); // 'sealed' - still exists
console.log(frozenObj.name); // 'frozen' - still exists

// Try to modify existing properties
sealedObj.count = 100;
frozenObj.count = 100;
console.log(sealedObj.count); // 100 - MODIFIED
console.log(frozenObj.count); // 0 - UNCHANGED

// Check status
console.log(Object.isSealed(sealedObj)); // true
console.log(Object.isFrozen(sealedObj)); // false
console.log(Object.isSealed(frozenObj)); // true
console.log(Object.isFrozen(frozenObj)); // true

Shallow Nature & Nested Objects

Both Object.seal() and Object.freeze() are shallow operations. They only affect the immediate properties of the object, not nested objects or arrays. This is a critical gotcha that catches many developers off guard.

const company = {
  name: 'TechCorp',
  employee: {
    name: 'Bob',
    salary: 50000
  },
  departments: ['Engineering', 'Sales']
};

Object.freeze(company);

// Top-level properties are frozen
company.name = 'NewCorp';
console.log(company.name); // 'TechCorp' - unchanged

// But nested objects are still mutable!
company.employee.salary = 75000;
console.log(company.employee.salary); // 75000 - CHANGED

company.departments.push('Marketing');
console.log(company.departments); // ['Engineering', 'Sales', 'Marketing']

// The reference itself can't be changed
company.employee = { name: 'Alice', salary: 60000 };
console.log(company.employee.name); // 'Bob' - reference unchanged

To achieve deep immutability, you need to recursively freeze all nested objects:

function deepFreeze(obj) {
  // Retrieve the property names defined on obj
  const propNames = Object.getOwnPropertyNames(obj);

  // Freeze properties before freezing self
  for (const name of propNames) {
    const value = obj[name];

    if (value && typeof value === 'object') {
      deepFreeze(value);
    }
  }

  return Object.freeze(obj);
}

const data = {
  settings: {
    theme: 'dark',
    notifications: {
      email: true,
      push: false
    }
  }
};

deepFreeze(data);

// Now nested objects are also frozen
data.settings.theme = 'light';
console.log(data.settings.theme); // 'dark' - unchanged

data.settings.notifications.push = true;
console.log(data.settings.notifications.push); // false - unchanged

The same principle applies to Object.seal()—you’d need a deepSeal function for nested objects.

Practical Use Cases & Best Practices

Use Object.freeze() when:

  • Creating application configuration objects that should never change
  • Defining true constants that represent immutable values
  • Preventing accidental mutations in functional programming patterns
  • Creating enums or constant lookup objects
// Application configuration
const APP_CONFIG = Object.freeze({
  API_VERSION: 'v2',
  BASE_URL: 'https://api.example.com',
  TIMEOUT: 30000,
  FEATURES: Object.freeze({
    ANALYTICS: true,
    BETA_FEATURES: false
  })
});

// Enum-like constants
const HTTP_STATUS = Object.freeze({
  OK: 200,
  CREATED: 201,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  NOT_FOUND: 404,
  SERVER_ERROR: 500
});

Use Object.seal() when:

  • You want a fixed schema but need to update values (like a state object)
  • Creating objects that should maintain their structure but allow data updates
  • Building APIs where you want to prevent property addition but allow updates
// State object with fixed structure
const appState = Object.seal({
  isLoading: false,
  user: null,
  error: null
});

// Can update values
appState.isLoading = true;
appState.user = { id: 1, name: 'Alice' };

// Can't add new state properties
appState.theme = 'dark'; // Fails - maintains schema integrity

Performance considerations: Both methods have minimal performance impact. The overhead is negligible for most applications. However, avoid sealing or freezing objects in tight loops or performance-critical paths.

TypeScript integration: These methods work well with TypeScript’s readonly modifier:

interface Config {
  readonly apiUrl: string;
  readonly timeout: number;
}

const config: Config = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000
});

// TypeScript prevents this at compile time
// config.timeout = 10000; // Error

// JavaScript prevents this at runtime

Strict mode: Always use strict mode when working with sealed or frozen objects. It converts silent failures into thrown errors, making bugs easier to catch:

'use strict';

const obj = Object.freeze({ value: 42 });
obj.value = 100; // TypeError: Cannot assign to read only property

Conclusion

Choose Object.freeze() when you need true immutability and don’t want any modifications to your object. It’s ideal for configuration objects, constants, and situations where any change would indicate a bug. Choose Object.seal() when you need a fixed structure but want to allow value updates, such as state objects with a defined schema.

Remember that both methods are shallow—nested objects require recursive application for deep immutability. In production code, combine these methods with strict mode and TypeScript for maximum safety. These tools won’t solve all mutability issues, but they’re valuable additions to your defensive programming toolkit that help prevent entire classes of bugs before they reach production.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.