{"record":{"id":"ece7e0bded18a2b6","repo":"BabylonJS/Babylon.js","slug":"failed-to-restore-original-property-descriptor","errorCode":null,"errorMessage":"Failed to restore original property descriptor \"${propertyKey.toString()}\" on object \"${target}\".","messagePattern":"Failed to restore original property descriptor \"(.+?)\" on object \"(.+?)\"\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/dev/inspector-v2/src/instrumentation/propertyInstrumentation.ts","lineNumber":164,"sourceCode":"                if (hooksForKey.length === 0) {\r\n                    hooksMap.delete(propertyKey);\r\n\r\n                    // If there are no more hooks for the target object, remove the hooks map from the WeakMap.\r\n                    if (hooksMap.size === 0) {\r\n                        InterceptorHooksMaps.delete(target);\r\n                    }\r\n\r\n                    const shouldRestorePropertyDescriptor =\r\n                        // If the property is owned by the target object, then we may have replaced an original property descriptor that needs to be restore.\r\n                        propertyOwner === target &&\r\n                        // But this is only the case if we found an existing property descriptor on the target object (hence the ownerAndDescriptor check),\r\n                        // or if the property value is not undefined, in which case we still want to retain the value that was set.\r\n                        (ownerAndDescriptor || target[propertyKey] !== undefined);\r\n                    // Otherwise, the property was inherited through the prototype chain, and so we can simply delete it from the target object.\r\n\r\n                    if (shouldRestorePropertyDescriptor) {\r\n                        if (!Reflect.defineProperty(target, propertyKey, propertyDescriptor)) {\r\n                            throw new Error(`Failed to restore original property descriptor \"${propertyKey.toString()}\" on object \"${target}\".`);\r\n                        }\r\n                    } else {\r\n                        if (!Reflect.deleteProperty(target, propertyKey)) {\r\n                            throw new Error(`Failed to delete transient property descriptor \"${propertyKey.toString()}\" on object \"${target}\".`);\r\n                        }\r\n                    }\r\n                }\r\n\r\n                isDisposed = true;\r\n            }\r\n        },\r\n    };\r\n}\r\n","sourceCodeStart":146,"sourceCodeEnd":178,"githubUrl":"https://github.com/BabylonJS/Babylon.js/blob/0592b347b8a4ee0236089ea86a749cacfdb266d8/packages/dev/inspector-v2/src/instrumentation/propertyInstrumentation.ts#L146-L178","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Avoid freezing/sealing the target while a property interceptor is active; dispose first, then freeze.","Check that no other code redefined the property during the interception window.","Perform dispose during teardown while the object is still extensible and the property configurable.","If the throw occurs, manually restore with Object.defineProperty(target, key, originalDescriptor) captured before interception, in a try/catch.","If restore is impossible, recreate the object from its state snapshot."],"exampleFix":"// before\nconst d = InterceptProperty(obj, \"value\", { afterSet: log });\nObject.freeze(obj);\nd.dispose(); // throws\n\n// after\nconst d = InterceptProperty(obj, \"value\", { afterSet: log });\nd.dispose();\nObject.freeze(obj);","handlingStrategy":"try-catch","validationCode":"// before dispose\nconst d = Reflect.getOwnPropertyDescriptor(target, key);\nif (!d || !d.configurable) console.warn(\"cannot restore descriptor; dispose later or rebuild\");\nif (Object.isFrozen(target)) console.warn(\"target frozen; deferred restore\");","typeGuard":"function isRestorable(target: object, key: PropertyKey): boolean {\n  if (Object.isFrozen(target)) return false;\n  const d = Reflect.getOwnPropertyDescriptor(target, key);\n  return !d || d.configurable;\n}","tryCatchPattern":"try {\n  disposable.dispose();\n} catch (e) {\n  if (e instanceof Error && /Failed to restore original property descriptor/.test(e.message)) {\n    try { Reflect.defineProperty(target, key, savedDescriptor); } catch { /* recreate object */ }\n  } else throw e;\n}","preventionTips":["Keep a copy of the original descriptor so you can restore manually if dispose throws.","Dispose watchers before freezing/hardening the object.","Avoid redefining watched properties with other libraries during interception.","Run disposals in teardown code that executes before cleanup/hardening phases.","Fail loudly in dev: log restore failures so shadowing wrappers are noticed."],"tags":["instrumentation","reflection","dispose","property-descriptor"],"backgroundTag":"property-descriptor-restore-failed","analyzedSha":"0592b347b8a4ee0236089ea86a749cacfdb266d8","analyzedAt":"2026-08-30T15:11:20.442Z","schemaVersion":2},"datasetVersion":"2026-08-30T18:17:15.746Z"}