dotnet/runtime · error · Error

must be a Function but was

Error message

${function_name} must be a Function but was ${typeof fn}

What it means

Thrown by mono_wasm_lookup_js_import in invoke-js.ts after successfully resolving the full dotted path: the final value (scope[fname]) must be typeof 'function', otherwise it throws with the actual typeof. So the path resolves to a non-function (object, string, undefined, number) and cannot be bound as a JSImport.

Solutions

  1. Make sure the last segment of the [JSImport] path refers to an actual function.
  2. Export the target as a named function in the JS module.
  3. Fix casing: JS is case-sensitive, a wrong-case name resolves to undefined.

Example fix

// before: export const parse = (s) => JSON.parse(s); imported as [JSImport("parse")]
// that works, but if imported a constant:
// export const DEFAULT = 42;  // typeof 'number' -> error
// after: expose a function
export const getDefault = () => DEFAULT;
Defensive patterns

Strategy: type-guard

Validate before calling

// verify the resolved value is callable
const v = scope[fname];
if (typeof v !== 'function') throw new TypeError(`${fname} is ${typeof v}, expected function`);

Type guard

function isFunctionRef(value: unknown): value is Function {
  return typeof value === 'function';
}

Prevention

When it happens

Trigger: A [JSImport("MyApp.value")] where value is a property/constant, not a function; the final name resolves to undefined because of a typo; the JS module exports a class/namespace object instead of a method.

Common situations: Importing a getter, constant, or class instance by mistake; mismatched casing in the function name; the JS module default export is an object whose methods are one level deeper.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/8ada58bb0c30519a. Report an issue: GitHub.

Appendix: source

Thrown at src/mono/browser/runtime/invoke-js.ts:413

    } 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 ${function_name}`);
        }
        scope = newscope;
    }

    const fname = parts[parts.length - 1];
    const fn = scope[fname];

    if (typeof (fn) !== "function") {
        throw new Error(`${function_name} 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 set_property (self: any, name: string, value: any): void {
    mono_check(self, "Null reference");
    self[name] = value;
}

export function get_property (self: any, name: string): any {
    mono_check(self, "Null reference");
    return self[name];
}

export function has_property (self: any, name: string): boolean {
    mono_check(self, "Null reference");

View on GitHub (pinned to 60108ba66e)