dotnet/runtime · error · Error
Cannot call synchronous C# method from inside a synchronous…
Error message
Cannot call synchronous C# method from inside a synchronous call to a JS method.
What it means
Thrown by call_delegate() when runtimeHelpers.isPendingSynchronousCall is true, meaning the JS engine is currently executing inside a synchronous callback FROM C# (a JSExport or delegate wrapper invoked by managed code). Attempting another synchronous call back into C# from within that callback would create a re-entrant synchronous round-trip. In multithreaded mode on the UI thread this would deadlock the message pump, so the runtime refuses it. The isPendingSynchronousCall flag is set in marshal-to-cs.ts delegate_wrapper and cleared in its finally block.
Solutions
- Break the synchronous re-entrancy: make the inner call asynchronous (use setTimeout or queueMicrotask to defer the C# call until after the current synchronous JS callback returns).
- Redesign the interop boundary so the JS callback does not need to call back into C# synchronously; collect the needed data before entering the callback.
- Move the nested call to a JSWebWorker where synchronous interop is handled differently.
- If truly necessary, use the async invoke path (invoke_async_jsexport) for the nested call instead of the sync path.
Example fix
// before (nested sync call inside a JSExport callback -> deadlock)
[JSExport]
function myJsFunction() {
return csharpDelegate(); // throws: inside a synchronous call
}
// after (defer the nested call)
[JSExport]
function myJsFunction() {
setTimeout(() => csharpDelegate(), 0); // deferred, no re-entrancy
return undefined;
} Defensive patterns
Strategy: validation
Validate before calling
// Avoid calling C# synchronously from within a JSExport callback
// Instead, always defer nested calls
function safeCallFromJsExport(fn: Function, ...args: any[]) {
// If we're inside a synchronous C# callback, defer
if (typeof runtimeHelpers !== 'undefined' && runtimeHelpers.isPendingSynchronousCall) {
setTimeout(() => fn(...args), 0);
} else {
fn(...args);
}
} Try / catch
try {
csharpDelegate(arg);
} catch (e) {
if (e.message.includes('inside a synchronous call')) {
// defer the call to break re-entrancy
setTimeout(() => csharpDelegate(arg), 0);
} else {
throw e;
}
} Prevention
- Never call back into C# synchronously from within a JSExport or JSImport callback.
- Use setTimeout(fn, 0) or queueMicrotask to defer nested interop calls.
- Design interop boundaries to be one-directional per synchronous frame.
- Track whether you are inside a JSExport callback and switch to deferred mode.
When it happens
Trigger: C# calls a JS function via JSExport/JSImport; inside that JS callback, the code calls back into C# synchronously (e.g., invokes another delegate or JSImport). This nested synchronous call hits the guard in call_delegate at line 181-183 because isPendingSynchronousCall is still true from the outer call.
Common situations: A JSExport method that calls a JS function, which in turn calls a C# delegate synchronously. Deeply nested interop chains. Event handler patterns where a JS event triggers C# which triggers JS which triggers C# again.
Related errors
- Cannot call synchronous C# methods.
- Argument to stringToInternedMonoStringRoot must be a string…
- Expected string argument, got
- External roots are not supported in multithreaded mode
- External roots are not supported when threads are enabled
AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10).
Data as JSON: /api/errors/fd70fbc56830346a.
Report an issue: GitHub.
Appendix: source
Thrown at src/mono/browser/runtime/managed-exports.ts:182
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);
}
if (arg2_converter) {View on GitHub (pinned to 60108ba66e)