{"record":{"id":"0dbf8fcd6cde5cf1","repo":"dotnet/runtime","slug":"cannot-call-synchronous-c-methods","errorCode":null,"errorMessage":"Cannot call synchronous C# methods.","messagePattern":"Cannot call synchronous C# methods\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/mono/browser/runtime/managed-exports.ts","lineNumber":180,"sourceCode":"            }\n        }\n        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);","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/dotnet/runtime/blob/290d5ab72cc1102813fbc0406fb186ceadabc340/src/mono/browser/runtime/managed-exports.ts#L162-L198","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconfig.jsThreadBlockingMode = 'PreventSynchronousJSExport';\nmyCsDelegate(args); // throws on UI thread\n// after\nconfig.jsThreadBlockingMode = 'WarnWhenBlockingWait';\n// or prefer: make the C# delegate return Task and await it","handlingStrategy":"validation","validationCode":"// Check the blocking mode and call site before invoking a sync delegate on the UI thread.\nconst isDefaultBlock = (config.jsThreadBlockingMode ?? 'PreventSynchronousJSExport') === 'PreventSynchronousJSExport';\nif (isDefaultBlock && typeof self !== 'undefined' && self.document /* UI thread */) {\n  throw new Error('Refusing sync delegate call on UI thread under PreventSynchronousJSExport; use an async Task export.');\n}","typeGuard":null,"tryCatchPattern":"try {\n  myCsDelegate(arg);\n} catch (e) {\n  if (String(e?.message) === 'Cannot call synchronous C# methods.') {\n    // fall back to an async Task-based export\n    return await dotnet.jsExports.MyMethodAsync(arg);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["threading","interop","synchronization"],"analyzedSha":"290d5ab72cc1102813fbc0406fb186ceadabc340","analyzedAt":"2026-08-06T19:57:01.276Z","schemaVersion":2},"datasetVersion":"2026-08-06T23:17:07.152Z"}