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
- Change jsThreadBlockingMode to ThrowWhenBlockingWait or WarnWhenBlockingWait to allow synchronous JSExport on the UI thread: dotnet.withConfig({ config: { jsThreadBlockingMode: 'ThrowWhenBlockingWait' } }).
- Make the C# delegate async (return Task) and await it from JS instead of calling synchronously.
- Run the interop on a JSWebWorker where synchronous calls are permitted.
- 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
- Set jsThreadBlockingMode to 'ThrowWhenBlockingWait' if you need synchronous interop on the UI thread.
- Prefer async delegate calls (return Task) from UI-thread code.
- Run interop-heavy synchronous operations on a JSWebWorker.
- Document which delegates are safe to call synchronously vs. async-only.
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
- Cannot call synchronous C# method from inside a synchronous…
- Invalid jsThreadBlockingMode
- Argument to stringToInternedMonoStringRoot must be a string…
- must be marked with 'BlazorWebAssemblyLazyLoad' item group…
- must be marked with 'BlazorWebAssemblyLazyLoad' item group…
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)