{"record":{"id":"f16635d093c8db48","repo":"BabylonJS/Babylon.js","slug":"at-least-one-hook-must-be-provided","errorCode":null,"errorMessage":"At least one hook must be provided.","messagePattern":"At least one hook must be provided\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/dev/inspector-v2/src/instrumentation/functionInstrumentation.ts","lineNumber":34,"sourceCode":"/**\r\n * Intercepts a function on an object and allows you to add hooks that will be called during function execution.\r\n * @param target The object containing the function to intercept.\r\n * @param propertyKey The key of the property that is a function (this is the function that will be intercepted).\r\n * @param hooks The hooks to call during the function execution.\r\n * @returns A disposable that removes the hooks when disposed and returns the object to its original state.\r\n */\r\n// This overload only matches when K is a specific literal key (not a union like keyof T)\r\nexport function InterceptFunction<T extends object, K extends keyof T>(\r\n    target: T,\r\n    propertyKey: string extends K ? never : number extends K ? never : symbol extends K ? never : K,\r\n    hooks: NonNullable<T[K]> extends (...args: infer Args) => unknown ? FunctionHooks<Args> : FunctionHooks\r\n): IDisposable;\r\n// Fallback overload for generic/dynamic cases where the function type cannot be inferred\r\nexport function InterceptFunction<T extends object>(target: T, propertyKey: keyof T, hooks: FunctionHooks): IDisposable;\r\n/** @internal */\r\nexport 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","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/BabylonJS/Babylon.js/blob/0592b347b8a4ee0236089ea86a749cacfdb266d8/packages/dev/inspector-v2/src/instrumentation/functionInstrumentation.ts#L16-L52","documentation":"InterceptFunction wraps an object's method so registered hooks run around calls, but it requires at least one hook (the guard checks hooks.afterCall) to be useful. If hooks.afterCall is missing the library throws immediately rather than installing a do-nothing interceptor. A second guard throws if the target property is not actually a function.","triggerScenarios":"Calling InterceptFunction(target, \"method\", {}) or with a hooks object lacking afterCall; constructing hooks dynamically and dropping the afterCall key; passing hooks typed loosely as any so an empty object slips through.","commonSituations":"Building instrumentation helpers where hooks are conditionally assembled and all conditions are false; refactoring that renamed afterCall (newer API shape) leaving an empty hooks object; copy-pasting an overload call with the wrong hooks shape.","solutions":["Provide at least an afterCall hook, e.g. InterceptFunction(target, \"play\", { afterCall: (r) => log(r) }).","Ensure the hooks object is built with afterCall always present, even if the callback is a no-op.","Confirm the property exists and is a function on the target to avoid the follow-up \"is not a function\" error.","Type hooks as FunctionHooks so TypeScript rejects objects missing afterCall."],"exampleFix":"// before\nInterceptFunction(soundtrack, \"play\", {}); // throws\n\n// after\nInterceptFunction(soundtrack, \"play\", {\n  afterCall: (result) => {\n    console.log(\"play called, returned:\", result);\n    return result;\n  }\n});","handlingStrategy":"validation","validationCode":"if (!hooks || typeof hooks.afterCall !== \"function\") {\n  throw new TypeError(\"InterceptFunction requires an afterCall hook\");\n}\nif (typeof target[propertyKey] !== \"function\") {\n  throw new TypeError(`Property \"${String(propertyKey)}\" is not a function`);\n}","typeGuard":"function hasValidHooks<T extends object>(t: T, k: keyof T, h: FunctionHooks): h is FunctionHooks & Required<Pick<FunctionHooks, \"afterCall\">> {\n  return !!h && typeof h.afterCall === \"function\" && typeof t[k] === \"function\";\n}","tryCatchPattern":"try {\n  const disposable = InterceptFunction(target, \"play\", hooks);\n  interceptors.push(disposable);\n} catch (e) {\n  if (e instanceof Error && (e.message.includes(\"hook must be provided\") || e.message.includes(\"is not a function\"))) {\n    console.error(\"Interception setup failed:\", e.message);\n  } else {\n    throw e;\n  }\n}","preventionTips":["Always include afterCall when constructing FunctionHooks, even as a pass-through.","Type hooks objects as FunctionHooks to catch missing keys at compile time.","Confirm the target method name exists before intercepting.","Dispose interceptor tokens on cleanup to avoid duplicate wrapping."],"tags":["instrumentation","hooks","invalid-argument"],"backgroundTag":"missing-required-callback","analyzedSha":"0592b347b8a4ee0236089ea86a749cacfdb266d8","analyzedAt":"2026-08-30T15:11:20.442Z","schemaVersion":2},"datasetVersion":"2026-08-30T18:17:15.746Z"}