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

  1. 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).
  2. 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.
  3. Move the nested call to a JSWebWorker where synchronous interop is handled differently.
  4. 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

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


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)