Question

Calling Async Methods from Synchronous Code in C#

csharpasynchronousasync-awaitsynchronous

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 Task to 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:

  • async and await are 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 Task is the buzzer
  • await means: 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.

Take Quiz

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>

public async Task<int> GetNumberAsync()
{
    await Task.Delay(500);
    return 42;
}

public int Run()
{
    return GetNumberAsync().GetAwaiter().GetResult();
}

Preferred option: Make the caller async too

public async Task RunAsync()
{
    await Foo();
}

This is usually the cleanest and safest solution.

Fire-and-forget only when appropriate

public void Run()
{
    _ = Foo();
}

This starts the task and does not wait for it. This is risky because:

  • exceptions may be missed
  • you do not know when it finishes
  • the process may exit before it completes

Use this only when you deliberately want background work and have proper error handling.

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:

  1. PrintMessage() is called.
  2. LoadMessageAsync() starts running.
  3. It reaches await Task.Delay(1000).
  4. A Task<string> is returned to the caller, representing unfinished work.
  5. GetAwaiter().GetResult() blocks the current thread until that task finishes.
  6. After 1 second, the delay completes.
  7. LoadMessageAsync() resumes and returns "Done".
  8. GetResult() gives that string back to PrintMessage().
  9. Console.WriteLine(message) prints Done.

This works, but notice that the caller thread was blocked the whole time. That is the main downside.

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.

Take Quiz

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

Validate early so you fail fast without starting tasks unnecessarily.

public async Task<string> DownloadAsync(string url)
{
    if (string.IsNullOrWhiteSpace(url))
        throw new ArgumentException("URL is required.");

    return await httpClient.GetStringAsync(url);
}

5. Handle exceptions intentionally

When blocking on async methods, exception behavior matters.

try
{
    Foo().GetAwaiter().GetResult();
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

Developers often prefer GetAwaiter().GetResult() over .Wait() or .Result because it throws the original exception directly instead of wrapping it in AggregateException.

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

public void Run()
{
    Foo();
}

This starts the task, but if it fails, you may never notice.

Better: only do this if you track/log errors explicitly.

Mistake 4: Mixing sync and async deep in the call chain

If many layers block on async calls, performance and debugging get worse.

Avoid: sync wrapper after sync wrapper.

Mistake 5: Ignoring environment-specific behavior

What works in a console app may fail in:

  • WinForms
  • WPF
  • ASP.NET classic

These environments may have a synchronization context that contributes to deadlocks.

Mistake 6: Returning void for async methods unless it is an event handler

Broken example:

public async void Foo()
{
    await Task.Delay(1000);
}

async void is hard to wait for and hard to test.

Better:

public async Task Foo()
{
    await Task.Delay(1000);
}

Comparisons

ApproachExampleBlocks thread?Exception behaviorRecommended?
awaitawait Foo();NoOriginal exceptionYes
.GetAwaiter().GetResult()Foo().GetAwaiter().GetResult();YesOriginal exceptionAcceptable only when necessary
.Wait()Foo().Wait();YesAggregateExceptionLess preferred
.ResultGetValueAsync().ResultYesAggregateExceptionLess preferred
Fire-and-forget_ = Foo();NoEasy to lose exceptionsOnly in special cases

await vs blocking

FeatureawaitBlocking (.Wait(), .Result)
Keeps thread freeYesNo
Scales betterYesNo
Deadlock riskLowerHigher
Easy exception handlingYesUsually worse
Best for modern C#YesNo

.Wait() vs .GetAwaiter().GetResult()

Feature.Wait().GetAwaiter().GetResult()
BlocksYesYes
Wraps exceptionsYesNo, usually throws original
ReadabilityCommon but olderOften preferred when blocking is unavoidable
Take Quiz

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 Task or Task<T> for async methods
  • Avoid async void except 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

  • async does not mean “new thread”
  • await does not block the thread
  • .Wait() and .Result do block the thread
  • Blocking on async is a workaround, not the ideal design

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 async Main or async methods.

Should I expose both sync and async methods in a library?

Sometimes yes, especially if callers need both styles. But avoid implementing sync methods by naively blocking on async code unless you understand the tradeoffs.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more