dotnet/runtime · error · Error

Cannot call synchronous C# methods.

Error message

Cannot call synchronous C# methods.

What it means

Thrown by call_delegate() on the UI thread in multithreaded mode when runtimeHelpers.config.jsThreadBlockingMode is set to PreventSynchronousJSExport (the default). Synchronous calls to C# delegates block the UI thread and would freeze the page; the runtime prevents this by throwing. This guard only fires when WasmEnableThreads is true and the current thread is the UI thread. On a JSWebWorker, synchronous calls are always allowed.

Solutions

  1. Change jsThreadBlockingMode to ThrowWhenBlockingWait or WarnWhenBlockingWait to allow synchronous JSExport on the UI thread: dotnet.withConfig({ config: { jsThreadBlockingMode: 'ThrowWhenBlockingWait' } }).
  2. Make the C# delegate async (return Task) and await it from JS instead of calling synchronously.
  3. Run the interop on a JSWebWorker where synchronous calls are permitted.
  4. Avoid calling the delegate from the UI thread; offload the work to a worker and post a message back.

Example fix

// before (synchronous delegate call on UI thread, default mode)
const result = csharpDelegate(arg1);

// after (configure to allow sync calls, or make it async)
const runtime = await dotnet
  .withConfig({ config: { jsThreadBlockingMode: 'ThrowWhenBlockingWait' } })
  .create();
// now synchronous delegate calls work, but .Wait() inside C# will throw
Defensive patterns

Strategy: validation

Validate before calling

// Check if synchronous calls are allowed before invoking
function canCallSync(): boolean {
  // Only an issue on UI thread in multithreaded mode with default blocking mode
  // If jsThreadBlockingMode is PreventSynchronousJSExport (default), sync calls throw
  return runtimeHelpers?.config?.jsThreadBlockingMode !== 'PreventSynchronousJSExport'
    || !WasmEnableThreads;
}
if (!canCallSync()) {
  // use async path instead
  await callAsync();
}

Try / catch

try {
  const result = csharpDelegate(arg);
} catch (e) {
  if (e.message === 'Cannot call synchronous C# methods.') {
    // fallback: make the call async
    await asyncWrapper(() => csharpDelegate(arg));
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Invoking a C# delegate via its JS proxy (returned from a JSImport or JSExport) synchronously on the main UI thread, with the runtime configured in multithreaded mode (WasmEnableThreads=true) and jsThreadBlockingMode at its default PreventSynchronousJSExport. The call_delegate function is the entry point for delegate invocation.

Common situations: Calling a synchronous C# delegate from a button click handler or other UI event on the main thread in a Blazor or raw dotnet-wasm app with threads enabled. Using a JSImport that returns a delegate and calling it immediately in a synchronous context.

Related errors


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

Appendix: 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 60108ba66e)