Question
I have a public asynchronous method like this:
public async Task Foo()
{
// asynchronous work
}
I want to call it from a synchronous method in C#. Most examples in the documentation show async methods calling other async methods, but my application is not fully built around asynchronous methods.
Is it possible to call an async method from a synchronous method? If so, what are the correct ways to do it, and what should I be careful about?
Short Answer
By the end of this page, you will understand how asynchronous methods can be called from synchronous code in C#, why this is sometimes risky, and which approaches are safest. You will also learn when to block on a Task, why deadlocks can happen, and why propagating async upward is usually the best design.
Concept
In C#, an async method usually returns Task or Task<T>. That return value represents work that may still be running.
A synchronous method does not use await, so it cannot naturally pause and resume while that work completes. Because of that, calling an async method from sync code usually means choosing one of these options:
- Block and wait for the
Taskto finish - Start the task without waiting if you truly do not care about completion
- Refactor the calling code to become async
The most important idea is this:
asyncandawaitare designed to avoid blocking threads- synchronous waiting (
.Wait(),.Result,.GetAwaiter().GetResult()) blocks the current thread - blocking can reduce performance and may cause deadlocks in some environments
Why this matters in real programming:
- In UI apps, blocking the main thread can freeze the interface
- In ASP.NET classic, blocking can lead to deadlocks or poor scalability
- In libraries and services, blocking wastes thread pool threads
- In console apps or small migration points, sync-over-async may be acceptable if done carefully
So yes, it is possible, but it is often a design tradeoff rather than the preferred solution.
Mental Model
Think of an async method as placing an order at a café and getting a buzzer.
- The
Taskis the buzzer awaitmeans: do other useful things until the buzzer goes off- synchronous waiting means: stand at the counter and refuse to move until your order is ready
Both approaches eventually get your food, but the second one wastes time and may block others. In some places, standing there also prevents the staff from finishing your order properly. That is similar to the deadlock problem when sync code blocks on async work.
Syntax and Examples
The core async method syntax looks like this:
public async Task Foo()
{
await Task.Delay(500);
}
A synchronous method cannot use await unless it is also marked async. So if you must call Foo() from sync code, common options are:
Option 1: Block until completion
public void Run()
{
Foo().GetAwaiter().GetResult();
}
This waits for Foo() to complete before continuing.
Option 2: Use .Wait()
public void Run()
{
Foo().Wait();
}
This also blocks, but it wraps exceptions in AggregateException, which is usually less convenient.
Option 3: Get a result from Task<T>
Step by Step Execution
Consider this example:
public async Task<string> LoadMessageAsync()
{
await Task.Delay(1000);
return "Done";
}
public void PrintMessage()
{
string message = LoadMessageAsync().GetAwaiter().GetResult();
Console.WriteLine(message);
}
Here is what happens step by step:
PrintMessage()is called.LoadMessageAsync()starts running.- It reaches
await Task.Delay(1000). - A
Task<string>is returned to the caller, representing unfinished work. GetAwaiter().GetResult()blocks the current thread until that task finishes.- After 1 second, the delay completes.
LoadMessageAsync()resumes and returns"Done".GetResult()gives that string back toPrintMessage().Console.WriteLine(message)prints .
Real World Use Cases
There are real cases where synchronous code must interact with async methods:
Legacy applications
Older codebases may expose synchronous interfaces but need to call newer async APIs.
Example:
- a legacy service class calls
HttpClient.GetStringAsync() - the public method signature cannot easily be changed yet
Console app entry points in older code
Before async Main became common, developers often had a synchronous entry point that needed to run async setup logic.
Framework boundaries
Sometimes you implement an interface that only provides synchronous methods, but the internal work is asynchronous.
Transitional refactoring
When converting a codebase gradually, a sync method may temporarily call async code until more of the call chain becomes async.
Testing or scripting utilities
Small tools may use synchronous wrappers around async methods for simplicity, especially where deadlock risks are low.
Real Codebase Usage
In real projects, developers usually follow these patterns:
1. Prefer async all the way
If one method becomes async, let callers become async too.
public async Task SaveAsync()
{
await repository.WriteAsync();
}
public async Task ProcessAsync()
{
await SaveAsync();
}
This avoids blocking and scales better.
2. Use sync wrappers carefully
Sometimes a synchronous API must remain available.
public void Save()
{
SaveAsync().GetAwaiter().GetResult();
}
This is common in migration code, but it should be a deliberate compromise.
3. Separate truly sync and truly async implementations
If possible, provide both rather than forcing sync-over-async.
public void Save()
{
repository.Write();
}
public Task SaveAsync()
{
return repository.WriteAsync();
}
4. Use guard clauses before async work
Common Mistakes
Mistake 1: Using .Result or .Wait() without understanding deadlocks
public void Run()
{
Foo().Wait();
}
This may work in some cases, but in UI apps or ASP.NET classic it can deadlock.
Better:
public void Run()
{
Foo().GetAwaiter().GetResult();
}
This is still blocking, but exception handling is cleaner. Even better, make the caller async.
Mistake 2: Assuming async means a separate thread
async does not automatically create a new thread.
public async Task Foo()
{
await Task.Delay(1000);
}
This is asynchronous, but not necessarily running on a dedicated background thread.
Mistake 3: Fire-and-forget without handling exceptions
Comparisons
| Approach | Example | Blocks thread? | Exception behavior | Recommended? |
|---|---|---|---|---|
await | await Foo(); | No | Original exception | Yes |
.GetAwaiter().GetResult() | Foo().GetAwaiter().GetResult(); | Yes | Original exception | Acceptable only when necessary |
.Wait() | Foo().Wait(); | Yes | AggregateException | Less preferred |
Cheat Sheet
// Async method
public async Task Foo()
{
await Task.Delay(1000);
}
// Preferred: async caller
public async Task RunAsync()
{
await Foo();
}
// If sync caller is required
public void Run()
{
Foo().GetAwaiter().GetResult();
}
Rules of thumb
- Prefer async all the way
- Use
TaskorTask<T>for async methods - Avoid
async voidexcept for event handlers - If you must block, prefer
GetAwaiter().GetResult()over.Wait()or.Result - Be careful in UI apps and ASP.NET classic because blocking can deadlock
- Fire-and-forget only when completion and exceptions are managed properly
Quick reminders
asyncdoes not mean “new thread”awaitdoes not block the thread
FAQ
Can I call an async method from a non-async method in C#?
Yes. You can block on the returned Task using GetAwaiter().GetResult(), .Wait(), or .Result. However, this is usually less safe than making the caller async.
What is the safest way to call async code from synchronous code?
If you absolutely must block, GetAwaiter().GetResult() is generally preferred over .Wait() and .Result because it does not wrap exceptions in AggregateException.
Why can calling async from sync code cause deadlocks?
Some environments use a synchronization context that async continuations try to resume on. If the current thread is blocked waiting, the continuation may never run.
Should I use .Result or .Wait()?
Usually no. They block the thread and wrap exceptions. GetAwaiter().GetResult() is typically better if blocking is unavoidable.
Is fire-and-forget a good solution?
Usually not by default. It can lose exceptions and makes program flow harder to reason about. Use it only for intentional background work.
Is it okay in a console app to block on async?
Often it works, especially in simple programs, but it still blocks the thread. If possible, prefer or async methods.
Mini Project
Description
Build a small C# example that shows both the recommended async approach and a synchronous wrapper around async code. This demonstrates how legacy synchronous code can interact with async operations, while making the tradeoffs visible.
Goal
Create a simple data-loading example where an async method returns a value after a delay, and call it from both an async method and a synchronous method.
Requirements
- Create an async method that returns a string using
Task<string>. - Call that method from an async method using
await. - Call the same method from a synchronous method using a blocking approach.
- Print the returned value in both cases.
- Keep the code valid C# and easy to run in a console app.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
C# Type Checking Explained: typeof vs GetType() vs is
Learn when to use typeof, GetType(), and is in C#. Understand exact type checks, inheritance, and safe type testing clearly.
C# Version Numbers Explained: C# vs .NET Framework and Why “C# 3.5” Is Incorrect
Learn the correct C# version numbers, how they map to .NET releases, and why terms like C# 3.5 are inaccurate and confusing.