{"record":{"id":"5b62b385ee2c9e7e","repo":"BabylonJS/Babylon.js","slug":"property-propertykey-tostring-of-object-5b62b3","errorCode":null,"errorMessage":"Property \"${propertyKey.toString()}\" of object \"${target}\" is readonly.","messagePattern":"Property \"(.+?)\" of object \"(.+?)\" is readonly\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/dev/inspector-v2/src/instrumentation/functionInstrumentation.ts","lineNumber":50,"sourceCode":"export function InterceptFunction<T extends object>(target: T, propertyKey: keyof T, hooks: FunctionHooks): IDisposable {\r\n    if (!hooks.afterCall) {\r\n        throw new Error(\"At least one hook must be provided.\");\r\n    }\r\n\r\n    const originalFunction = Reflect.get(target, propertyKey, target) as (...args: any) => any;\r\n    if (typeof originalFunction !== \"function\") {\r\n        throw new Error(`Property \"${propertyKey.toString()}\" of object \"${target}\" is not a function.`);\r\n    }\r\n\r\n    // Make sure the property is configurable and writable, otherwise it is immutable and cannot be intercepted.\r\n    const propertyDescriptor = Reflect.getOwnPropertyDescriptor(target, propertyKey);\r\n    if (propertyDescriptor) {\r\n        if (!propertyDescriptor.configurable) {\r\n            throw new Error(`Property \"${propertyKey.toString()}\" of object \"${target}\" is not configurable.`);\r\n        }\r\n\r\n        if (propertyDescriptor.writable === false || (propertyDescriptor.writable === undefined && !propertyDescriptor.set)) {\r\n            throw new Error(`Property \"${propertyKey.toString()}\" of object \"${target}\" is readonly.`);\r\n        }\r\n    }\r\n\r\n    // Get or create the hooks map for the target object.\r\n    let hooksMap = InterceptorHooksMaps.get(target);\r\n    if (!hooksMap) {\r\n        InterceptorHooksMaps.set(target, (hooksMap = new Map()));\r\n    }\r\n\r\n    // Get or create the hooks array for the property key.\r\n    let hooksForKey = hooksMap.get(propertyKey);\r\n    if (!hooksForKey) {\r\n        hooksMap.set(propertyKey, (hooksForKey = []));\r\n        if (\r\n            // Replace the function with a new one that calls the hooks in addition to the original function.\r\n            !Reflect.set(target, propertyKey, function (this: unknown, ...args: unknown[]) {\r\n                const result = Reflect.apply(originalFunction, this, args);\r\n                for (const { afterCall } of hooksForKey!) {\r","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/BabylonJS/Babylon.js/blob/0592b347b8a4ee0236089ea86a749cacfdb266d8/packages/dev/inspector-v2/src/instrumentation/functionInstrumentation.ts#L32-L68","documentation":"To install an interception, the library must overwrite the property with a replacement function and later restore it, so the property must be writable (or have a setter). If the descriptor shows writable === false, or it is a non-writable, setter-less accessor (writable undefined and no set), the property is readonly and interception is refused. This is thrown only when the property exists directly on the target (a descriptor was found).","triggerScenarios":"Calling InterceptFunction on an own property defined with writable: false (e.g. via Object.defineProperty or Object.freeze), or an accessor with only a getter (no setter) so a new value cannot be assigned.","commonSituations":"Frozen state/config objects whose callback methods were frozen for safety; getter-only properties exposing a function (get handler() {...}); readonly module exports; constants frozen in library code you are trying to monkey-patch.","solutions":["Define the property as writable (Object.defineProperty(obj, key, {value: fn, writable: true, configurable: true})) before intercepting.","Add a setter to accessor-only properties, or restructure so the function is a writable data property.","Avoid Object.freeze on objects you intend to instrument; freeze a copy instead and intercept the unfrozen original.","Wrap the function at its definition site (pass the instrumented function in) instead of replacing the readonly property."],"exampleFix":"// before\nconst cfg = Object.freeze({ onClick: fn });\nInterceptFunction(cfg, 'onClick', hooks); // throws: readonly\n// after\nconst cfg = { onClick: fn }; // do not freeze if interception is needed\nInterceptFunction(cfg, 'onClick', hooks);","handlingStrategy":"validation","validationCode":"function isWritable(target, propertyKey) {\n  const d = Reflect.getOwnPropertyDescriptor(target, propertyKey);\n  if (!d) return true; // inherited or absent: handled elsewhere\n  return d.writable === true || (d.writable === undefined && typeof d.set === 'function');\n}\nif (!isWritable(target, 'play')) throw new TypeError('target.play is readonly');","typeGuard":"function hasWritableDescriptor(target: object, key: PropertyKey): boolean {\n  const d = Object.getOwnPropertyDescriptor(target, key);\n  return d === undefined || d.writable === true || (d.writable === undefined && typeof d.set === 'function');\n}","tryCatchPattern":"try {\n  InterceptFunction(target, propertyKey, hooks);\n} catch (e) {\n  if (String((e as Error)?.message).includes('is readonly')) {\n    console.warn(`Cannot intercept ${String(propertyKey)}: property is readonly`);\n    return null;\n  }\n  throw e;\n}","preventionTips":["Verify descriptor writable/set before intercepting own properties.","Do not Object.freeze objects containing methods you intend to hook.","Prefer writable data properties over getter-only accessors for hookable callbacks.","If you must hook a readonly property, wrap it at the definition/call site instead."],"tags":["instrumentation","property-descriptor","readonly","frozen-object"],"backgroundTag":"property-is-readonly","analyzedSha":"0592b347b8a4ee0236089ea86a749cacfdb266d8","analyzedAt":"2026-08-30T15:11:20.442Z","schemaVersion":2},"datasetVersion":"2026-08-30T18:17:15.746Z"}