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

  1. Open the JS module in devtools and confirm the exact export at that path is a function (typeof exports.compute === 'function').
  2. Correct the [JSImport("globalThis.path.to.fn")] string to match the real function location, or re-export the function under the expected name.
  3. Ensure the module is actually loaded first via JSHost.ImportAsync("moduleName") before invoking.
  4. 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

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


AI-assisted analysis of dotnet/runtime@290d5ab72c (2026-08-06). Data as JSON: /api/errors/76e0d0531eb008e3. Report an issue: GitHub.