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 intoInnerException. - 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: