{"record":{"id":"fd70fbc56830346a","repo":"dotnet/runtime","slug":"cannot-call-synchronous-c-method-from-inside-a-sy","errorCode":null,"errorMessage":"Cannot call synchronous C# method from inside a synchronous call to a JS method.","messagePattern":"Cannot call synchronous C# method from inside a synchronous call to a JS method\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mono/browser/runtime/managed-exports.ts","lineNumber":182,"sourceCode":"        if (error) {\n            marshal_exception_to_cs(arg2, error);\n        }\n        invoke_async_jsexport(runtimeHelpers.ioThreadTID, managedExports.CompleteTask, args, size);\n    } finally {\n        if (loaderHelpers.is_runtime_running()) Module.stackRestore(sp);\n\n    }\n}\n\n// the marshaled signature is: TRes? CallDelegate<T1,T2,T3,TRes>(GCHandle callback, T1? arg1, T2? arg2, T3? arg3)\nexport 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) {\n    loaderHelpers.assert_runtime_running();\n    if (WasmEnableThreads) {\n        if (monoThreadInfo.isUI) {\n            if (runtimeHelpers.config.jsThreadBlockingMode == JSThreadBlockingMode.PreventSynchronousJSExport) {\n                throw new Error(\"Cannot call synchronous C# methods.\");\n            } else if (runtimeHelpers.isPendingSynchronousCall) {\n                throw new Error(\"Cannot call synchronous C# method from inside a synchronous call to a JS method.\");\n            }\n        }\n    }\n    const sp = Module.stackSave();\n    try {\n        const size = 6;\n        const args = alloc_stack_frame(size);\n\n        const arg1 = get_arg(args, 2);\n        set_arg_type(arg1, MarshalerType.Object);\n        set_gc_handle(arg1, callback_gc_handle);\n        // payload arg numbers are shifted by one, the real first is a gc handle of the callback\n\n        if (arg1_converter) {\n            const arg2 = get_arg(args, 3);\n            arg1_converter(arg2, arg1_js);\n        }\n        if (arg2_converter) {","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/dotnet/runtime/blob/60108ba66eb7d1d12f595480091b4ad80a24b172/src/mono/browser/runtime/managed-exports.ts#L164-L200","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (nested sync call inside a JSExport callback -> deadlock)\n[JSExport]\nfunction myJsFunction() {\n  return csharpDelegate(); // throws: inside a synchronous call\n}\n\n// after (defer the nested call)\n[JSExport]\nfunction myJsFunction() {\n  setTimeout(() => csharpDelegate(), 0); // deferred, no re-entrancy\n  return undefined;\n}","handlingStrategy":"validation","validationCode":"// Avoid calling C# synchronously from within a JSExport callback\n// Instead, always defer nested calls\nfunction safeCallFromJsExport(fn: Function, ...args: any[]) {\n  // If we're inside a synchronous C# callback, defer\n  if (typeof runtimeHelpers !== 'undefined' && runtimeHelpers.isPendingSynchronousCall) {\n    setTimeout(() => fn(...args), 0);\n  } else {\n    fn(...args);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  csharpDelegate(arg);\n} catch (e) {\n  if (e.message.includes('inside a synchronous call')) {\n    // defer the call to break re-entrancy\n    setTimeout(() => csharpDelegate(arg), 0);\n  } else {\n    throw e;\n  }\n}","preventionTips":["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."],"tags":["threading","interop","reentrancy","deadlock-prevention"],"backgroundTag":null,"analyzedSha":"60108ba66eb7d1d12f595480091b4ad80a24b172","analyzedAt":"2026-08-10T18:54:11.478Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}