{"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/60108ba66eb7d1d12f595480091b4ad80a24b172/src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts#L277-L313","documentation":"lookupJsImport() successfully traversed the dotted path but the final resolved value (scope[fname]) is not a function (typeof fn !== 'function'). Since [JSImport] must bind to a callable JS function, this is rejected. The error reports the full name and the actual typeof so you can see whether it was 'undefined', 'object', 'string', etc.","triggerScenarios":"The final segment of a [JSImport] dotted name resolves to a non-function: a property that is undefined (name typo on the leaf), an object, a number/string constant, or a value that hasn't been initialized yet at bind time.","commonSituations":"Typo in the function (leaf) name; the export was renamed in the JS module; the binding targets a property/constant rather than an exported function; tree-shaking removed the export; the function is conditionally defined and not present when binding runs.","solutions":["Confirm the leaf name in [JSImport(\"...\", \"module\")] is an exported function in the JS module; inspect typeof importedModule[leafName].","Fix typos or align with the current export name after a refactor.","Ensure the export is a function (export function foo() {}) and not an object/constant the C# side mistook for a callable.","If the function is assigned lazily, ensure the module is fully evaluated before the binding occurs (JSHost.ImportAsync awaited)."],"exampleFix":"// before - C# binds a renamed export\n[JSImport(\"compute\", \"m\")]\n// m.js: export const compute = (x) => x; is fine, but if it was renamed to 'run':\n\n// after - align names\n[JSImport(\"run\", \"m\")]\n// m.js: export function run(x) { return x; }","handlingStrategy":"type-guard","validationCode":"// Confirm the leaf of the dotted path is a function before binding.\nfunction resolveFunction(scope: any, dotted: string): Function | undefined {\n  const parts = dotted.split(\".\");\n  let s = scope;\n  for (let i = 0; i < parts.length - 1; i++) s = s?.[parts[i]];\n  const fn = s?.[parts[parts.length - 1]];\n  return typeof fn === \"function\" ? fn : undefined;\n}\n\nconst fn = resolveFunction(mod, \"a.b.run\");\nif (!fn) console.error(\"'run' is not a function on a.b\");","typeGuard":"function isFunctionAt(scope: any, dotted: string): boolean {\n  const parts = dotted.split(\".\");\n  let s = scope;\n  for (let i = 0; i < parts.length - 1; i++) s = s?.[parts[i]];\n  return typeof s?.[parts[parts.length - 1]] === \"function\";\n}","tryCatchPattern":"null","preventionTips":["Ensure the leaf name in [JSImport] is an exported function in the JS module.","Verify the export wasn't renamed or removed by a refactor.","Confirm the export is a function, not a constant/object with the same name.","Await JSHost.ImportAsync before bindings run so lazy exports are present."],"tags":["jsinterop","jsimport","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"}