dotnet/runtime · error · Error
${functionName} must be a Function but was ${typeof fn}
Error message
${functionName} must be a Function but was ${typeof fn} What it means
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.
Source
Thrown at src/native/libs/System.Runtime.InteropServices.JavaScript.Native/interop/invoke-js.ts:295
} else if (parts[0] === "globalThis") {
scope = globalThis;
parts.shift();
}
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
const newscope = scope[part];
if (!newscope) {
throw new Error(`${part} not found while looking up ${functionName}`);
}
scope = newscope;
}
const fname = parts[parts.length - 1];
const fn = scope[fname];
if (typeof (fn) !== "function") {
throw new Error(`${functionName} must be a Function but was ${typeof fn}`);
}
// if the function was already bound to some object it would stay bound to original object. That's good.
return fn.bind(scope);
}
export function invokeJSFunction(functionJSHandle: JSHandle, args: JSMarshalerArguments): void {
assertRuntimeRunning();
const boundFn = getJSObjectFromJSHandle(functionJSHandle);
dotnetAssert.fastCheck(boundFn && typeof (boundFn) === "function" && boundFn[boundJsFunctionSymbol], () => `Bound function handle expected ${functionJSHandle}`);
args = fixupPointer(args, 0);
boundFn(args);
}
export function setProperty(self: any, name: string, value: any): void {
dotnetAssert.check(self, "Null reference");
self[name] = value;
}View on GitHub (pinned to 290d5ab72c)
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.
Example fix
// before
[JSImport("globalThis.config.settings")] // settings is an object
static partial string GetSettings();
// after
[JSImport("globalThis.config.getSettings")] // getSettings is a function
static partial string GetSettings(); Defensive patterns
Strategy: type-guard
Validate before calling
// Before exposing a function to C#, assert it is callable
const path = "globalThis.fns.compute";
const parts = path.replace(/^globalThis\./, "").split(".");
let scope: any = globalThis;
for (const p of parts) scope = scope?.[p];
if (typeof scope !== "function") {
throw new TypeError(`${path} resolved to ${typeof scope}, expected function`);
} Type guard
const isCallableByName = (path: string): boolean => {
const parts = path.replace(/^(globalThis\.|INTERNAL\.)/, "").split(".");
let scope: any = globalThis;
for (const p of parts) { scope = scope?.[p]; if (scope == null) return false; }
return typeof scope === "function";
}; Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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).
Related errors
- ${part} not found while looking up ${function_name}
- ${function_name} must be a Function but was ${typeof fn}
- ${part} not found while looking up ${functionName}
- Cannot call synchronous C# methods.
- Cannot call synchronous C# method from inside a synchronous
AI-assisted analysis of dotnet/runtime@290d5ab72c (2026-08-06).
Data as JSON: /api/errors/76e0d0531eb008e3.
Report an issue: GitHub.