{"record":{"id":"76e0d0531eb008e3","repo":"dotnet/runtime","slug":"functionname-must-be-a-function-but-was-typeo","errorCode":null,"errorMessage":"${functionName} must be a Function but was ${typeof fn}","messagePattern":"(.+?) must be a Function but was (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts","lineNumber":295,"sourceCode":"    } else if (parts[0] === \"globalThis\") {\n        scope = globalThis;\n        parts.shift();\n    }\n\n    for (let i = 0; i < parts.length - 1; i++) {\n        const part = parts[i];\n        const newscope = scope[part];\n        if (!newscope) {\n            throw new Error(`${part} not found while looking up ${functionName}`);\n        }\n        scope = newscope;\n    }\n\n    const fname = parts[parts.length - 1];\n    const fn = scope[fname];\n\n    if (typeof (fn) !== \"function\") {\n        throw new Error(`${functionName} must be a Function but was ${typeof fn}`);\n    }\n\n    // if the function was already bound to some object it would stay bound to original object. That's good.\n    return fn.bind(scope);\n}\n\nexport function invokeJSFunction(functionJSHandle: JSHandle, args: JSMarshalerArguments): void {\n    assertRuntimeRunning();\n    const boundFn = getJSObjectFromJSHandle(functionJSHandle);\n    dotnetAssert.fastCheck(boundFn && typeof (boundFn) === \"function\" && boundFn[boundJsFunctionSymbol], () => `Bound function handle expected ${functionJSHandle}`);\n    args = fixupPointer(args, 0);\n    boundFn(args);\n}\n\nexport function setProperty(self: any, name: string, value: any): void {\n    dotnetAssert.check(self, \"Null reference\");\n    self[name] = value;\n}","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/dotnet/runtime/blob/290d5ab72cc1102813fbc0406fb186ceadabc340/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts#L277-L313","documentation":"Thrown by lookupJsImport when resolving a JS function name (requested from C# via [JSImport] / JSHost.callBack) where the scope chain resolves to the final member, but that member is not callable. The runtime walked globalThis / INTERNAL / an imported ES module and found the property yet typeof returned something other than \"function\" (e.g. \"string\", \"number\", \"object\", \"undefined\"-like getter). It is a hard contract failure: the C# side asked to invoke a function that does not exist as a function on the JS side.","triggerScenarios":"A [JSImport] attribute points at a fully-qualified JS path (e.g. \"globalThis.mathHelper.compute\") whose last segment names a non-function property (a constant, object, or null). Also when a JSHost.ImportAsync module exports a value rather than a function and C# tries to call it, or when the path is correct but the binding was overwritten (e.g. re-export of a non-function).","commonSituations":"Renaming a JS export without updating the [JSImport] string; shadowing an exported function with a config object of the same name; minification/tree-shaking dropping the function; referencing a property that only exists behind a conditional export (NODE vs browser).","solutions":["Open the JS module in devtools and confirm the exact export at that path is a function (typeof exports.compute === 'function').","Correct the [JSImport(\"globalThis.path.to.fn\")] string to match the real function location, or re-export the function under the expected name.","Ensure the module is actually loaded first via JSHost.ImportAsync(\"moduleName\") before invoking.","If the target is a method on an instance, wrap it in an exported function instead of pointing [JSImport] at the instance property."],"exampleFix":"// before\n[JSImport(\"globalThis.config.settings\")] // settings is an object\nstatic partial string GetSettings();\n\n// after\n[JSImport(\"globalThis.config.getSettings\")] // getSettings is a function\nstatic partial string GetSettings();","handlingStrategy":"type-guard","validationCode":"// Before exposing a function to C#, assert it is callable\nconst path = \"globalThis.fns.compute\";\nconst parts = path.replace(/^globalThis\\./, \"\").split(\".\");\nlet scope: any = globalThis;\nfor (const p of parts) scope = scope?.[p];\nif (typeof scope !== \"function\") {\n    throw new TypeError(`${path} resolved to ${typeof scope}, expected function`);\n}","typeGuard":"const isCallableByName = (path: string): boolean => {\n    const parts = path.replace(/^(globalThis\\.|INTERNAL\\.)/, \"\").split(\".\");\n    let scope: any = globalThis;\n    for (const p of parts) { scope = scope?.[p]; if (scope == null) return false; }\n    return typeof scope === \"function\";\n};","tryCatchPattern":null,"preventionTips":["Keep JS export names and [JSImport] strings in one shared constant file so renames stay in sync.","Add a startup self-test that resolves every imported path and asserts typeof === 'function'.","Run [JSImport] calls under the Debug build so dotnetAssert catches lookup issues early."],"tags":["jsimport","interop","function-lookup","typescript"],"analyzedSha":"290d5ab72cc1102813fbc0406fb186ceadabc340","analyzedAt":"2026-08-06T19:57:01.276Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}