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
- Make the C# entry point async (Task-based) and await it from JS instead of calling a synchronous delegate.
- If you accept the deadlock risk, set config.jsThreadBlockingMode to 'ThrowWhenBlockingWait', 'WarnWhenBlockingWait', or 'DangerousAllowBlockingWait'.
- 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
- Prefer Task-based async [JSExport] methods over synchronous delegates invoked from JS.
- If you must call sync, set jsThreadBlockingMode deliberately (not the default) and understand the deadlock risk.
- Keep synchronous interop off the UI thread by running it on a worker.
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
- Cannot call synchronous C# method from inside a synchronous
- ${part} not found while looking up ${function_name}
- ${function_name} must be a Function but was ${typeof fn}
- Invalid jsThreadBlockingMode
- NotImplementedException: bigint
AI-assisted analysis of dotnet/runtime@290d5ab72c (2026-08-06).
Data as JSON: /api/errors/0dbf8fcd6cde5cf1.
Report an issue: GitHub.