Proposal: Object.unseal and Object.unfreeze using a secret Symbol

Hello everyone!

I’d like to share an early-stage idea for a reversible Object.seal() and Object.freeze() API, using a secret key mechanism (such as a Symbol) to restore the object’s original mutability.

Proposal overview:

const secret = Symbol();

// Seals or freezes with an optional secret
Object.seal(obj, secret);   // reversible seal
Object.freeze(obj, secret)  // reversible freeze

// Later, restores previous state
Object.unseal(obj, secret)
Object.unfreeze(obj, secret)

If the correct secret is not provided, Object.unseal() and Object.unfreeze() throw an error.

Use cases

  • Frameworks or libraries that want to protect an object during validation or construction, and then unlock it.
  • Test environments or mutation guards.
  • Developer tools and instrumented runtimes.

Use case example

// my-api.js
function MyApi() {
    const items = []
    const secret = Symbol()
    Object.freeze(items, secret)

    return {
        items,
        add: (item) => {
            Object.unfreeze(items, secret)
            items.push(item)
            Object.freeze(items, secret)
        },
    }
}

// consumer.js
const api = MyApi()
api.add('item1')
console.log(api.items) // ['item1']
api.items.push('item3') // Throws an error

Why not using Proxy?

To replicate this behavior using a Proxy, you need to intercept and restrict all mutating operations manually:

function MyApi() {
    const items = []

    const proxy = new Proxy(items, {
        get(target, prop, receiver) {
            return Reflect.get(target, prop, receiver)
        },
        set() {
            return false
        },
        deleteProperty() {
            return false
        },
        // You would also need to intercept defineProperty, setPrototypeOf, etc.
    })

    return {
        items: proxy,
        add: (item) => {
            items.push(item)
        },
    }
}
  • Proxies require intercepting every mutating trap, and forgetting one introduces a hole.
  • It’s more verbose and harder to reason about.
  • Object.freeze/unfreeze clearly expresses intentional and scoped mutability.
  • Proxy traps have runtime overhead.
  • The secret-based approach uses less memory, storing only a symbol, while proxies must keep all traps and the proxy object in memory.
  • Iteration methods like for...in and Object.keys may behave inconsistently depending on the proxy handler.
  • Proxies are not always fully transparent, which can cause unexpected behavior.
  • From a developer experience standpoint, plain objects support full property introspection, autocompletion, and static analysis.
    Proxies, on the other hand, obscure the object's shape — making tools like DevTools, autocomplete, and type systems less effective or unusable.
  • Both approaches rely on keeping something private — the target in a Proxy, or the secret in unfreeze. But only the secret model enforces immutability at the object level, even if the reference leaks.

Open questions

  • Should Object.isSealed() / Object.isFrozen() report as sealed even when the object is reversible?
  • How would this affect internal engine mechanics?
  • Would engines have any issues storing the Symbol as metadata?
  • Would it be better to introduce a new method, such as Object.protect() / Object.unprotect()?

Feedback

I’d love to hear your thoughts on this direction. Especially:

  • Viability concerns
  • Implementation challenges (engine/runtime)
  • Potential use cases I haven’t considered
  • Interest from potential champions

If this idea resonates, I’d be happy to draft a full proposal repo and spec outline.

Thanks!

Worth noting that this would violate one of the core invariants of the object model:

"If [[IsExtensible]] returns false, all future calls to [[IsExtensible]] on the target must return false."

These invariants were carefully considered and many members of the committee work hard to ensure that are not broken so I think it is unlikely for a proposal to weaken them.

1 Like

What would be the point of protecting something, just to then leave it completely unprotected?

If by "something" you mean anything in life, you might want to protect yourself with a bulletproof vest—and then take it off when you're safe at home.

If you're referring to the context of an object in JavaScript, you might want to prevent the consumer API from mutating the object.

If the goal is simpler code then I would probably create a concrete type to represent this, something like:

class LockableData {
  #data = new Map();
  #locked = false;
  #secret;

  constructor(secret) { this.#secret = secret }

  get(key) { return this.#data.get(key) }

  set(key, val) {
    if (this.#locked) throw new Error("locked");
    this.#data.set(key, val);
  }

  lock(secret) {
    if (this.#locked) throw new Error("locked");
    if (secret !== this.#secret) throw new Error("wrong secret");
    this.#locked = true;
    return () => { this.#locked = false; };
  }  
}

Instead of using an exotic object.

1 Like

I think this would add a lot of complexity to the object model in engines for relatively little benefit, with that cost potentially affecting even code which doesn't use this new kind of object, so it is unlikely to go anywhere. Performance of the object implementation in engines is crucially important which means there should be an extremely high bar for making it more complicated.

Perhaps the specific solution doesn't work, but I do find the problem to be solved intriguing - the idea of having a piece of data that, from outside the class you only want to provide read access to it, but from inside the class you want to be able to modify the data.

There could be other ways to solve a problem like this. We could, for example, provide a "read-only view" into an object - basically a proxy, but we provide a high-level API to make these views so developers don't have to manually configure the proxy.

Or something. Not really convinced on that option either, but it does seem like it would be nice to have some way of exposing data without also granting the ability to edit it.

1 Like

You provide a frozen object, and then copy it to a mutable one later?

The goal is to have the ability to create read-only plain objects outside the class or function that creates them, without relying on complex syntax, proxies, or duplicating data.

I thought that Object.unfreeze would be the easiest way to achieve this without introducing extra syntax or more complex changes. But as you pointed out, it violates the specification, and engines are optimized to work with non-extensible objects.

I understand, that my solution is not the best aproach. But I still think that having read-only data/objects outside the constructor is a powerful idea.

Then you end up with duplicate data, which is difficult to maintain at the API level, doubles the memory usage, and can introduce runtime overhead when copying large objects.