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
.Resultcan lead toAggregateException(ifAsyncMethodthrows). 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
asyncmethod 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.Resultin a thread that has aSynchronizationContextcan 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
.Resultor.Wait()often throwsAggregateExceptionif 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
asyncmethod remain async and call it withawaitfrom anasyncmethod. - Fallback: If forced to call it from a synchronous context (and you cannot change the API), use
.GetAwaiter().GetResult()or.Resultcautiously, aware of potential deadlocks and performance hits.