{"record":{"id":"8ada58bb0c30519a","repo":"dotnet/runtime","slug":"function-name-must-be-a-function-but-was-type","errorCode":null,"errorMessage":"${function_name} 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/mono/browser/runtime/invoke-js.ts","lineNumber":413,"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 ${function_name}`);\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(`${function_name} 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 set_property (self: any, name: string, value: any): void {\n    mono_check(self, \"Null reference\");\n    self[name] = value;\n}\n\nexport function get_property (self: any, name: string): any {\n    mono_check(self, \"Null reference\");\n    return self[name];\n}\n\nexport function has_property (self: any, name: string): boolean {\n    mono_check(self, \"Null reference\");","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/dotnet/runtime/blob/290d5ab72cc1102813fbc0406fb186ceadabc340/src/mono/browser/runtime/invoke-js.ts#L395-L431","documentation":"Thrown by mono_wasm_lookup_js_import after successfully resolving the final property of a [JSImport] name, when that resolved value is not a function (typeof !== 'function'). It signals the attribute points to a non-callable export such as a constant, object, or undefined.","triggerScenarios":"Produced when the last segment of a [JSImport] name resolves to a non-function value. E.g. [JSImport(\"myMod.config\")] where `config` is an object/string, or the export does not exist on the module (resolves to undefined).","commonSituations":"Attribute points to a property/constant instead of a function; the export was renamed; the default export is referenced as if it were a named function; the module exports an object whose member is not a function.","solutions":["Confirm the final segment of the JSImport path resolves to a function (typeof check in the JS module).","Update the JSImport attribute to the correct function export name.","If interop needs a non-function value, use a wrapper JS function that returns it instead of importing the value directly.","Ensure the module is imported via JSHost.ImportAsync so the export is populated."],"exampleFix":"// before: importing a non-function export\n// JS: export const config = { url: '...' };\n// C#: [JSImport(\"myMod.config\")] static partial string GetUrl();\n\n// after: expose a function\n// JS: export function getUrl() { return config.url; }\n// C#: [JSImport(\"myMod.getUrl\")] static partial string GetUrl();","handlingStrategy":"type-guard","validationCode":"const fn = scope[fname];\nif (typeof fn !== 'function') {\n  // the export is missing or not callable; fix the JS export or the attribute\n}","typeGuard":"function isCallable(scope: any, name: string): boolean {\n  return typeof scope?.[name] === 'function';\n}","tryCatchPattern":null,"preventionTips":["Export functions (not constants) for [JSImport] targets, or wrap values in accessor functions.","Add a unit test asserting each [JSImport] target resolves to a function in the JS module.","Re-check exports after refactoring JS modules."],"tags":["jsimport","interop","types"],"analyzedSha":"290d5ab72cc1102813fbc0406fb186ceadabc340","analyzedAt":"2026-08-06T19:57:01.276Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}