{"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/290d5ab72cc1102813fbc0406fb186ceadabc340/src/mono/browser/runtime/managed-exports.ts#L164-L200","documentation":"Inside call_delegate, the runtime checks runtimeHelpers.isPendingSynchronousCall. While managed code is already synchronously calling into JS, any further synchronous call from JS back into C# is refused, because the managed thread is blocked waiting on the JS call and cannot re-enter without deadlock or stack corruption.","triggerScenarios":"Re-entrancy: C# synchronously calls a JS method (JSImport/JSExport sync), and inside that JS callback the code synchronously invokes another C# method/delegate while isPendingSynchronousCall is true.","commonSituations":"A JS interop callback that itself calls back into C# (e.g. a JS function passed to C# that, during the sync call, invokes another exported C# method); event-driven code that nests sync interop.","solutions":["Defer the inner C# call: wrap it in queueMicrotask/setTimeout(0) or a Promise so it runs after the outer sync call unwinds.","Restructure the interop so JS returns a value to C# instead of re-entering C# synchronously.","Make the C# side async to break the synchronous re-entry chain."],"exampleFix":"// before\n// inside a sync JSImport callback:\notherCsExport(); // throws: re-entrant sync call\n// after\nqueueMicrotask(() => otherCsExport());","handlingStrategy":"validation","validationCode":"// Do not re-enter C# synchronously from within a sync JS callback. Detect you are inside one by tracking context.\nlet insideSyncJsCallback = false;\nfunction aroundJsCallback(fn) {\n  return (...args) => { const prev = insideSyncJsCallback; insideSyncJsCallback = true; try { return fn(...args); } finally { insideSyncJsCallback = prev; } };\n}\nfunction callCsSafe(fn, ...args) {\n  if (insideSyncJsCallback) { queueMicrotask(() => fn(...args)); return null; }\n  return fn(...args);\n}","typeGuard":null,"tryCatchPattern":"try {\n  otherCsExport();\n} catch (e) {\n  if (String(e?.message).startsWith('Cannot call synchronous C# method from inside')) {\n    queueMicrotask(() => otherCsExport());\n    return;\n  }\n  throw e;\n}","preventionTips":["Never call back into C# synchronously from a JS function that C# is calling synchronously.","Defer nested interop with queueMicrotask/Promise.resolve().then().","Design interop so JS returns values to C# rather than invoking C# mid-callback."],"tags":["threading","interop","reentrancy","deadlock"],"analyzedSha":"290d5ab72cc1102813fbc0406fb186ceadabc340","analyzedAt":"2026-08-06T19:57:01.276Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}