{"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/60108ba66eb7d1d12f595480091b4ad80a24b172/src/mono/browser/runtime/managed-exports.ts#L162-L198","documentation":"Thrown by call_delegate() on the UI thread in multithreaded mode when runtimeHelpers.config.jsThreadBlockingMode is set to PreventSynchronousJSExport (the default). Synchronous calls to C# delegates block the UI thread and would freeze the page; the runtime prevents this by throwing. This guard only fires when WasmEnableThreads is true and the current thread is the UI thread. On a JSWebWorker, synchronous calls are always allowed.","triggerScenarios":"Invoking a C# delegate via its JS proxy (returned from a JSImport or JSExport) synchronously on the main UI thread, with the runtime configured in multithreaded mode (WasmEnableThreads=true) and jsThreadBlockingMode at its default PreventSynchronousJSExport. The call_delegate function is the entry point for delegate invocation.","commonSituations":"Calling a synchronous C# delegate from a button click handler or other UI event on the main thread in a Blazor or raw dotnet-wasm app with threads enabled. Using a JSImport that returns a delegate and calling it immediately in a synchronous context.","solutions":["Change jsThreadBlockingMode to ThrowWhenBlockingWait or WarnWhenBlockingWait to allow synchronous JSExport on the UI thread: dotnet.withConfig({ config: { jsThreadBlockingMode: 'ThrowWhenBlockingWait' } }).","Make the C# delegate async (return Task) and await it from JS instead of calling synchronously.","Run the interop on a JSWebWorker where synchronous calls are permitted.","Avoid calling the delegate from the UI thread; offload the work to a worker and post a message back."],"exampleFix":"// before (synchronous delegate call on UI thread, default mode)\nconst result = csharpDelegate(arg1);\n\n// after (configure to allow sync calls, or make it async)\nconst runtime = await dotnet\n  .withConfig({ config: { jsThreadBlockingMode: 'ThrowWhenBlockingWait' } })\n  .create();\n// now synchronous delegate calls work, but .Wait() inside C# will throw","handlingStrategy":"validation","validationCode":"// Check if synchronous calls are allowed before invoking\nfunction canCallSync(): boolean {\n  // Only an issue on UI thread in multithreaded mode with default blocking mode\n  // If jsThreadBlockingMode is PreventSynchronousJSExport (default), sync calls throw\n  return runtimeHelpers?.config?.jsThreadBlockingMode !== 'PreventSynchronousJSExport'\n    || !WasmEnableThreads;\n}\nif (!canCallSync()) {\n  // use async path instead\n  await callAsync();\n}","typeGuard":null,"tryCatchPattern":"try {\n  const result = csharpDelegate(arg);\n} catch (e) {\n  if (e.message === 'Cannot call synchronous C# methods.') {\n    // fallback: make the call async\n    await asyncWrapper(() => csharpDelegate(arg));\n  } else {\n    throw e;\n  }\n}","preventionTips":["Set jsThreadBlockingMode to 'ThrowWhenBlockingWait' if you need synchronous interop on the UI thread.","Prefer async delegate calls (return Task) from UI-thread code.","Run interop-heavy synchronous operations on a JSWebWorker.","Document which delegates are safe to call synchronously vs. async-only."],"tags":["threading","interop","ui-thread","deadlock-prevention","configuration"],"backgroundTag":null,"analyzedSha":"60108ba66eb7d1d12f595480091b4ad80a24b172","analyzedAt":"2026-08-10T18:54:11.478Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}