{"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/60108ba66eb7d1d12f595480091b4ad80a24b172/src/mono/browser/runtime/invoke-js.ts#L395-L431","documentation":"Thrown by mono_wasm_lookup_js_import in invoke-js.ts after successfully resolving the full dotted path: the final value (scope[fname]) must be typeof 'function', otherwise it throws with the actual typeof. So the path resolves to a non-function (object, string, undefined, number) and cannot be bound as a JSImport.","triggerScenarios":"A [JSImport(\"MyApp.value\")] where value is a property/constant, not a function; the final name resolves to undefined because of a typo; the JS module exports a class/namespace object instead of a method.","commonSituations":"Importing a getter, constant, or class instance by mistake; mismatched casing in the function name; the JS module default export is an object whose methods are one level deeper.","solutions":["Make sure the last segment of the [JSImport] path refers to an actual function.","Export the target as a named function in the JS module.","Fix casing: JS is case-sensitive, a wrong-case name resolves to undefined."],"exampleFix":"// before: export const parse = (s) => JSON.parse(s); imported as [JSImport(\"parse\")]\n// that works, but if imported a constant:\n// export const DEFAULT = 42;  // typeof 'number' -> error\n// after: expose a function\nexport const getDefault = () => DEFAULT;","handlingStrategy":"type-guard","validationCode":"// verify the resolved value is callable\nconst v = scope[fname];\nif (typeof v !== 'function') throw new TypeError(`${fname} is ${typeof v}, expected function`);","typeGuard":"function isFunctionRef(value: unknown): value is Function {\n  return typeof value === 'function';\n}","tryCatchPattern":null,"preventionTips":["Ensure the last segment of the [JSImport] path is a function export.","Watch for case mismatches in function names (JS is case-sensitive).","Re-run binding tests after any JS module refactor."],"tags":["jsimport","interop","binding","type-mismatch"],"backgroundTag":null,"analyzedSha":"60108ba66eb7d1d12f595480091b4ad80a24b172","analyzedAt":"2026-08-10T18:54:11.478Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}