dotnet/runtime · error · Error

Cannot call synchronous C# methods.

Error message

Cannot call synchronous C# methods.

What it means

call_delegate invokes a C# delegate synchronously. With threads enabled, when the call runs on the JS UI thread and config.jsThreadBlockingMode is PreventSynchronousJSExport (the default set in config.ts:203), the runtime refuses the call to avoid blocking the UI thread and risking deadlock. This is the safest default mode and intentionally blocks synchronous re-entry into managed code.

Source

Thrown at src/mono/browser/runtime/managed-exports.ts:180

            }
        }
        if (error) {
            marshal_exception_to_cs(arg2, error);
        }
        invoke_async_jsexport(runtimeHelpers.ioThreadTID, managedExports.CompleteTask, args, size);
    } finally {
        if (loaderHelpers.is_runtime_running()) Module.stackRestore(sp);

    }
}

// the marshaled signature is: TRes? CallDelegate<T1,T2,T3,TRes>(GCHandle callback, T1? arg1, T2? arg2, T3? arg3)
export function call_delegate (callback_gc_handle: GCHandle, arg1_js: any, arg2_js: any, arg3_js: any, res_converter?: MarshalerToJs, arg1_converter?: MarshalerToCs, arg2_converter?: MarshalerToCs, arg3_converter?: MarshalerToCs) {
    loaderHelpers.assert_runtime_running();
    if (WasmEnableThreads) {
        if (monoThreadInfo.isUI) {
            if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) {
                throw new Error("Cannot call synchronous C# methods.");
            } else if (runtimeHelpers.isPendingSynchronousCall) {
                throw new Error("Cannot call synchronous C# method from inside a synchronous call to a JS method.");
            }
        }
    }
    const sp = Module.stackSave();
    try {
        const size = 6;
        const args = alloc_stack_frame(size);

        const arg1 = get_arg(args, 2);
        set_arg_type(arg1, MarshalerType.Object);
        set_gc_handle(arg1, callback_gc_handle);
        // payload arg numbers are shifted by one, the real first is a gc handle of the callback

        if (arg1_converter) {
            const arg2 = get_arg(args, 3);
            arg1_converter(arg2, arg1_js);

View on GitHub (pinned to 290d5ab72c)

Solutions

  1. Make the C# entry point async (Task-based) and await it from JS instead of calling a synchronous delegate.
  2. If you accept the deadlock risk, set config.jsThreadBlockingMode to 'ThrowWhenBlockingWait', 'WarnWhenBlockingWait', or 'DangerousAllowBlockingWait'.
  3. Move the interop off the UI thread by running it on a worker thread where synchronous JSExport is permitted.

Example fix

// before
config.jsThreadBlockingMode = 'PreventSynchronousJSExport';
myCsDelegate(args); // throws on UI thread
// after
config.jsThreadBlockingMode = 'WarnWhenBlockingWait';
// or prefer: make the C# delegate return Task and await it
Defensive patterns

Strategy: validation

Validate before calling

// Check the blocking mode and call site before invoking a sync delegate on the UI thread.
const isDefaultBlock = (config.jsThreadBlockingMode ?? 'PreventSynchronousJSExport') === 'PreventSynchronousJSExport';
if (isDefaultBlock && typeof self !== 'undefined' && self.document /* UI thread */) {
  throw new Error('Refusing sync delegate call on UI thread under PreventSynchronousJSExport; use an async Task export.');
}

Try / catch

try {
  myCsDelegate(arg);
} catch (e) {
  if (String(e?.message) === 'Cannot call synchronous C# methods.') {
    // fall back to an async Task-based export
    return await dotnet.jsExports.MyMethodAsync(arg);
  }
  throw e;
}

Prevention

When it happens

Trigger: A C# delegate handed to JS (e.g. via a JSImport callback / RegisterJSFunction) that JS then invokes synchronously while executing on the main/UI thread, under the default jsThreadBlockingMode=PreventSynchronousJSExport.

Common situations: Default dotnet-wasm threading config; calling back into C# synchronously from a DOM event handler, requestAnimationFrame, or a JS callback on the UI thread, expecting synchronous re-entrant interop.

Related errors


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