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

  1. Confirm the leaf name in [JSImport("...", "module")] is an exported function in the JS module; inspect typeof importedModule[leafName].
  2. Fix typos or align with the current export name after a refactor.
  3. Ensure the export is a function (export function foo() {}) and not an object/constant the C# side mistook for a callable.
  4. 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

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


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)