How to call asynchronous method from synchronous method in C#?
Calling an async method from a synchronous method can be tricky in C# because the async
/await
model is fundamentally designed to propagate asynchrony up the call stack. While the most recommended approach is to make the higher-level method async (so you can await
), sometimes you’re constrained by an existing synchronous API or an interface that you can’t change. Below are some ways to handle it, along with important caveats.
1. Call .GetAwaiter().GetResult()
This pattern avoids a potential AggregateException
and throws the original exception if the task fails:
public void SyncMethod() { // Synchronously wait for an async call to finish var result = AsyncMethod().GetAwaiter().GetResult(); Console.WriteLine(result); } public async Task<string> AsyncMethod() { await Task.Delay(1000); return "Async result"; }
- Pros: Preserves the original exception (rather than wrapping it in
AggregateException
). - Cons: This is effectively a blocking wait; the current thread is tied up until
AsyncMethod()
completes. If you call this on a UI thread (e.g., in a Windows Forms or WPF application) or in an ASP.NET context with aSynchronizationContext
, you risk a deadlock ifAsyncMethod()
tries to resume on the same context.
2. Use .Result
A common—but not ideal—approach is to directly access the .Result
property on the Task
:
public void SyncMethod() { // Potentially riskier way var result = AsyncMethod().Result; Console.WriteLine(result); }
- Pros: Simple, one-line.
- Cons: Calling
.Result
can lead toAggregateException
(ifAsyncMethod
throws). Also susceptible to the same deadlock pitfalls if you’re on a context that doesn’t allow re-entry (e.g., a UI thread).
3. Use Task.Run
in Some Contexts
If you absolutely must do async work from a sync method, one way to mitigate deadlock risk is to offload the call to another thread using Task.Run
. However, this may or may not fit your scenario:
public void SyncMethod() { var result = Task.Run(async () => await AsyncMethod()).Result; Console.WriteLine(result); }
- Pros: This can avoid deadlocks because the
async
method will resume on a thread pool thread instead of the original calling thread’s context. - Cons: You’re still blocking the caller thread. Also, if you rely on the original SynchronizationContext for UI updates, the code inside
AsyncMethod()
won’t have that context while it’s running.
4. Restructure to Use async
All the Way
Whenever possible, the best practice is to propagate async
up the call chain rather than forcing a synchronous call. For example, if you have:
public void MainEntryPoint() { // Instead of calling a sync method that calls async, // just make MainEntryPoint async too, if possible. var result = DoAsyncWork().GetAwaiter().GetResult(); } public async Task<string> DoAsyncWork() { await Task.Delay(1000); return "Done"; }
You can often change MainEntryPoint
into:
public async Task MainEntryPointAsync() { var result = await DoAsyncWork(); Console.WriteLine(result); }
- Pros: Avoids blocking, avoids deadlocks, and makes the code simpler in the long run.
- Cons: This may require a breaking change if the synchronous signature is locked by an interface or used by external callers.
Key Concerns
-
Deadlocks
- In a UI application (WPF/WinForms) or ASP.NET classic (pre-Core), calling
.Wait()
or.Result
in a thread that has aSynchronizationContext
can lead to deadlock. - If the async method attempts to resume on the same context while the thread is blocked, it’ll wait forever.
- In a UI application (WPF/WinForms) or ASP.NET classic (pre-Core), calling
-
Exception Wrapping
- Using
.Result
or.Wait()
often throwsAggregateException
if the async method fails. .GetAwaiter().GetResult()
unwraps this into the original exception, which may be more user-friendly.
- Using
-
Performance
- Forcing an async operation to run synchronously can hurt performance and responsiveness because you block the current thread.
- If it’s a UI thread, the UI becomes unresponsive until the async work completes.
-
Design
- Microsoft’s recommended approach is to follow the “async all the way” principle. If you control both the caller and callee, prefer making everything
async
.
- Microsoft’s recommended approach is to follow the “async all the way” principle. If you control both the caller and callee, prefer making everything
Strengthen Your C# and Async/Await Skills
Asynchronous programming can be challenging, especially in larger applications. If you’re aiming to level up your knowledge of C# and learn best coding practices, consider exploring these resources on DesignGurus.io:
Additionally, check out the DesignGurus.io YouTube channel for free tutorials and in-depth discussions around coding interviews, system design fundamentals, and advanced programming tips.
Bottom Line
- Preferred: Let the
async
method remain async and call it withawait
from anasync
method. - Fallback: If forced to call it from a synchronous context (and you cannot change the API), use
.GetAwaiter().GetResult()
or.Result
cautiously, aware of potential deadlocks and performance hits.