Asynchronous Call from Synchronous Code in C#: Battle of Approaches

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

How to call an asynchronous method from synchronous code in C#? Meat, not water



Imagine: you're sitting in the evening coding, finishing up an old console utility in C#. Everything works synchronously, like clockwork. And then — bam! — you need to call an API that's only made with async Task. You're not going to rewrite the whole program, are you? A familiar pain, right?



The question «How to call asynchronous method from synchronous method in C#» is one of the most popular on Stack Overflow. And for good reason. There, in the comments, are tons of advice from «just use .Result and don't worry» to «are you crazy, you'll get a deadlock!». Let's break it down: which approaches actually work, and which are a path to hell.



Why doesn't just «await» work?



If you try to write:

public void SyncMethod(){    await DoSomethingAsync(); // Error CS4032}

The compiler will simply refuse to compile it. await can only be used inside a method marked with the async modifier. And async void is a whole different story, which we'll also cover.



So, we have an asynchronous method:

public async Task<string> GetDataAsync(){    using var httpClient = new HttpClient();    return await httpClient.GetStringAsync("https://api.example.com/data");}


And there's a synchronous method from which we want to call it. Let's go through the options.



Approach 1: GetAwaiter().GetResult() — hacker chic



The most popular and, arguably, the most elegant way:

public string CallSync(){    return GetDataAsync().GetAwaiter().GetResult();}


Why it's cool:

  • Does not generate an AggregateException (unlike .Result). If an error occurs inside the async method, you get its original type, not a wrapper.
  • The code reads well, without unnecessary try-catch blocks.


But there's a catch: if your synchronous method executes in a context with SynchronizationContext (for example, in a UI thread of WinForms or WPF), you risk getting a deadlock — a mutual blocking of threads. Thread A waits for the async task to complete, and the task cannot complete because it's waiting for thread A to become free. A total mess.



How to avoid it: Use .ConfigureAwait(false) in the async method itself. But that changes the API, not just the calling code.



Approach 2: Task.Run(async () => await ...).GetAwaiter() — offloading to the thread pool



If you can't modify the async method but want to guarantee avoiding a deadlock — offload the task to the thread pool:

public string CallSyncSafe(){    return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();}


The magic: Task.Run runs the delegate on a thread from the ThreadPool, which has no SynchronizationContext. A deadlock is impossible. Even if the original method uses ConfigureAwait(true).



Downside: An extra context switch. For hundreds of calls — not okay. For one-off calls — perfect.



Approach 3: .Result or .Wait() — grandma's method



public string CallSyncOldSchool(){    return GetDataAsync().Result; // Or .Wait()}


What's bad:

  • On error, it throws AggregateException. To get to the actual error, you need to dig into InnerException.
  • Same risk of deadlock in a UI context.
  • The code looks like a hack from 2012.


This approach is a red flag for code reviews. If I see this in a pull request — I'll reject it.



Approach 4: AsyncHelper or custom RunSync



Experienced developers sometimes write their own helpers. For example, like this:

                

Blogs

Book Recommendations