dotnet/runtime · error · Error
must be a Function but was
Error message
${functionName} must be a Function but was ${typeof fn} What it means
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.
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).
Example fix
// before - C# binds a renamed export
[JSImport("compute", "m")]
// m.js: export const compute = (x) => x; is fine, but if it was renamed to 'run':
// after - align names
[JSImport("run", "m")]
// m.js: export function run(x) { return x; } Defensive patterns
Strategy: type-guard
Validate before calling
// Confirm the leaf of the dotted path is a function before binding.
function resolveFunction(scope: any, dotted: string): Function | undefined {
const parts = dotted.split(".");
let s = scope;
for (let i = 0; i < parts.length - 1; i++) s = s?.[parts[i]];
const fn = s?.[parts[parts.length - 1]];
return typeof fn === "function" ? fn : undefined;
}
const fn = resolveFunction(mod, "a.b.run");
if (!fn) console.error("'run' is not a function on a.b"); Type guard
function isFunctionAt(scope: any, dotted: string): boolean {
const parts = dotted.split(".");
let s = scope;
for (let i = 0; i < parts.length - 1; i++) s = s?.[parts[i]];
return typeof s?.[parts[parts.length - 1]] === "function";
} Try / catch
null
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- must be a Function but was
- not found while looking up
- not found while looking up
- JSObject proxy is not supported for
- JSObject proxy is not supported for
AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10).
Data as JSON: /api/errors/76e0d0531eb008e3.
Report an issue: GitHub.
Appendix: 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 60108ba66e)