BabylonJS/Babylon.js · error

Failed to restore original property descriptor "${propertyKe

Error message

Failed to restore original property descriptor "${propertyKey.toString()}" on object "${target}".

What it means

When the last hook for a property is disposed, InterceptProperty restores the original property descriptor captured at interception time using Reflect.defineProperty. This error means the restore was rejected, leaving the getter/setter wrapper (and hooks map entry already removed) in place, so the property stays permanently intercepted.

Source

Thrown at packages/dev/inspector-v2/src/instrumentation/propertyInstrumentation.ts:164

                if (hooksForKey.length === 0) {
                    hooksMap.delete(propertyKey);

                    // If there are no more hooks for the target object, remove the hooks map from the WeakMap.
                    if (hooksMap.size === 0) {
                        InterceptorHooksMaps.delete(target);
                    }

                    const shouldRestorePropertyDescriptor =
                        // If the property is owned by the target object, then we may have replaced an original property descriptor that needs to be restore.
                        propertyOwner === target &&
                        // But this is only the case if we found an existing property descriptor on the target object (hence the ownerAndDescriptor check),
                        // or if the property value is not undefined, in which case we still want to retain the value that was set.
                        (ownerAndDescriptor || target[propertyKey] !== undefined);
                    // Otherwise, the property was inherited through the prototype chain, and so we can simply delete it from the target object.

                    if (shouldRestorePropertyDescriptor) {
                        if (!Reflect.defineProperty(target, propertyKey, propertyDescriptor)) {
                            throw new Error(`Failed to restore original property descriptor "${propertyKey.toString()}" on object "${target}".`);
                        }
                    } else {
                        if (!Reflect.deleteProperty(target, propertyKey)) {
                            throw new Error(`Failed to delete transient property descriptor "${propertyKey.toString()}" on object "${target}".`);
                        }
                    }
                }

                isDisposed = true;
            }
        },
    };
}

View on GitHub (pinned to 0592b347b8)

Solutions

  1. Avoid freezing/sealing the target while a property interceptor is active; dispose first, then freeze.
  2. Check that no other code redefined the property during the interception window.
  3. Perform dispose during teardown while the object is still extensible and the property configurable.
  4. If the throw occurs, manually restore with Object.defineProperty(target, key, originalDescriptor) captured before interception, in a try/catch.
  5. If restore is impossible, recreate the object from its state snapshot.

Example fix

// before
const d = InterceptProperty(obj, "value", { afterSet: log });
Object.freeze(obj);
d.dispose(); // throws

// after
const d = InterceptProperty(obj, "value", { afterSet: log });
d.dispose();
Object.freeze(obj);
Defensive patterns

Strategy: try-catch

Validate before calling

// before dispose
const d = Reflect.getOwnPropertyDescriptor(target, key);
if (!d || !d.configurable) console.warn("cannot restore descriptor; dispose later or rebuild");
if (Object.isFrozen(target)) console.warn("target frozen; deferred restore");

Type guard

function isRestorable(target: object, key: PropertyKey): boolean {
  if (Object.isFrozen(target)) return false;
  const d = Reflect.getOwnPropertyDescriptor(target, key);
  return !d || d.configurable;
}

Try / catch

try {
  disposable.dispose();
} catch (e) {
  if (e instanceof Error && /Failed to restore original property descriptor/.test(e.message)) {
    try { Reflect.defineProperty(target, key, savedDescriptor); } catch { /* recreate object */ }
  } else throw e;
}

Prevention

When it happens

Trigger: Disposing an InterceptProperty disposable when the target has been frozen/sealed since interception, the property was redefined as non-configurable by other code while hooked, or a new own data property replaced the accessor in a way that blocks redefining with the original descriptor.

Common situations: Test teardown after Object.freeze on state objects; disposing watchers on hot-reloaded modules whose descriptors changed; interactions with other monkey-patching libraries that redefine the same property; strict-mode hardened objects (e.g. frozen globals).

Related errors


AI-assisted analysis of BabylonJS/Babylon.js@0592b347b8 (2026-08-30). Data as JSON: /api/errors/ece7e0bded18a2b6. Report an issue: GitHub.