C#
Asynchronous Programming
Synchronous Method
Async/Await
Programming Tips

How to call asynchronous method from synchronous method in C?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Calling an asynchronous method from synchronous C# code is possible, but it is usually a sign that you are at a boundary in the design, not in the middle of a healthy async flow. The safest rule is still "async all the way," because blocking on a Task can cause deadlocks, wasted threads, and confusing exception behavior if you do it casually.

Prefer Async Propagation When You Can

If you control both the caller and callee, the best fix is to make the caller asynchronous too.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        string value = await LoadMessageAsync();
9        Console.WriteLine(value);
10    }
11
12    static async Task<string> LoadMessageAsync()
13    {
14        await Task.Delay(100);
15        return "done";
16    }
17}

This avoids the whole problem. There is no blocking, no sync-over-async bridge, and exceptions flow naturally through await.

In modern .NET applications, this is usually the right answer for web handlers, background jobs, and console applications.

If You Must Block, Use GetAwaiter().GetResult()

Sometimes you are stuck at a synchronous boundary such as legacy interfaces, constructors that cannot be async, or older framework entry points. In those cases, GetAwaiter().GetResult() is usually the least bad blocking option.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Demo
5{
6    static void Main()
7    {
8        string value = LoadMessageAsync().GetAwaiter().GetResult();
9        Console.WriteLine(value);
10    }
11
12    static async Task<string> LoadMessageAsync()
13    {
14        await Task.Delay(100);
15        return "done";
16    }
17}

This still blocks the calling thread, but it unwraps exceptions more cleanly than Task.Result or Task.Wait().

Why Result and Wait() Are Risky

The real danger is not that blocking is always illegal. The danger is blocking in an environment with a synchronization context that expects continuations to resume on the same thread, such as older UI and ASP.NET contexts.

Consider this pattern:

csharp
1public string GetValueSynchronously()
2{
3    return LoadValueAsync().Result;
4}

If LoadValueAsync awaits work and then tries to resume on a thread that is now blocked by .Result, you can deadlock. That is why sync-over-async code often fails in WinForms, WPF, and older ASP.NET applications even when it seems fine in a console app.

GetAwaiter().GetResult() does not magically remove all risk, but it is usually the preferred bridge when blocking is unavoidable.

Reduce Risk Inside Library Code

If you are writing lower-level async code that may be called from many environments, use ConfigureAwait(false) where resuming on the original context is unnecessary.

csharp
1using System.Net.Http;
2using System.Threading.Tasks;
3
4static async Task<string> DownloadAsync(HttpClient client)
5{
6    await Task.Delay(50).ConfigureAwait(false);
7    return await client.GetStringAsync("https://example.com").ConfigureAwait(false);
8}

This can reduce deadlock risk when someone blocks on your method upstream, though it is not a license for sloppy sync-over-async design. The better structural fix is still to keep the call chain asynchronous.

Use Explicit Boundary Patterns

If the synchronous boundary is unavoidable, isolate it. One small bridge is easier to reason about than scattered .Result calls across the codebase.

For example, expose an async API internally and keep one synchronous wrapper at the outer edge:

csharp
1using System.Threading.Tasks;
2
3public class MessageService
4{
5    public Task<string> GetMessageAsync()
6    {
7        return Task.FromResult("hello");
8    }
9
10    public string GetMessage()
11    {
12        return GetMessageAsync().GetAwaiter().GetResult();
13    }
14}

This makes the compromise visible. It also gives you one place to remove later when the surrounding code becomes async-capable.

Common Pitfalls

The most common mistake is using .Result or .Wait() everywhere because they appear convenient. That hides async boundaries and makes deadlocks more likely.

Another issue is blaming await itself for the deadlock. The actual problem is usually blocking a thread that an awaited continuation needs in order to resume.

Constructors are another trap. If initialization must be asynchronous, prefer a factory method such as CreateAsync rather than forcing network or file I/O into a blocking constructor.

Finally, do not assume code that works in a console app is automatically safe in WPF, WinForms, or legacy ASP.NET. Synchronization context behavior changes the risk profile.

Summary

  • The best solution is to make the caller async and propagate await upward.
  • If you must block, GetAwaiter().GetResult() is usually preferable to .Result or .Wait().
  • Blocking on async work can deadlock in UI and older ASP.NET environments.
  • Use ConfigureAwait(false) in library code when context capture is unnecessary.
  • Keep any sync-over-async bridge isolated at a clear application boundary.

Course illustration
Course illustration

All Rights Reserved.