Question

Run Async Task<T> Methods Synchronously in C# Without Deadlocks

csharpasynchronousc#-5.0async-await

Question

I am learning async and await and need to consume an asynchronous method from synchronous code. How can I obtain the result safely?

public async Task<Customers> GetCustomersAsync()
{
    return await Service.GetCustomersAsync();
}

The normal asynchronous usage is:

public async Task LoadCustomersAsync()
{
    customerList = await GetCustomersAsync();
}

I tried blocking with Wait(), RunSynchronously(), and a loop that checks task status, but these approaches either fail or can hang in a WPF application. Why does RunSynchronously() throw RunSynchronously may not be called on a task unbound to a delegate, and what is the correct approach?

Short Answer

You will learn why asynchronous code should usually remain asynchronous, why blocking is dangerous on UI threads, and which limited techniques can bridge synchronous and asynchronous code at application boundaries.

Concept

Task<T> represents work that will eventually produce a value of type T. await is the preferred way to consume that value because it does not block the current thread while the work is in progress.

In a WPF application, the UI runs on a dispatcher thread. By default, an await in UI code captures that dispatcher context. When the awaited operation completes, its continuation is scheduled back to the UI thread.

A deadlock can happen when the UI thread does this:

  1. Starts an async operation.
  2. Blocks with .Wait(), .Result, or .GetAwaiter().GetResult().
  3. Prevents the UI thread from processing the continuation needed for the async operation to finish.
  4. Waits forever for a task whose continuation cannot run.

Therefore, the normal rule is: use await all the way up the call chain.

Task.RunSynchronously() does not mean “wait for this existing async task.” It is intended for a Task created with a delegate, such as new Task(action), that has not started yet. A task returned by an async method is already managed by that method and is not an unstarted delegate-backed task, which explains the exception.

Mental Model

Think of the UI thread as a cashier at a service desk.

  • await means: “Start the request, then continue serving other customers. Tell me when the result is ready.”
  • .Wait() means: “I will stand at the desk and refuse to move until the result arrives.”

If the result must be delivered by the same cashier, nobody can deliver it because the cashier is blocked waiting. That is a deadlock.

An async Task<T> is a delivery receipt, not the delivered package. await lets the application continue while the package is on its way.

Take Quiz

Syntax and Examples

The preferred pattern is to give asynchronous methods an Async suffix and await them.

public Task<Customers> GetCustomersAsync()
{
    return Service.GetCustomersAsync();
}

public async Task LoadCustomersAsync()
{
    customerList = await GetCustomersAsync();
}

The wrapper does not need async and await when it only returns another task. Returning the service task directly is equivalent here.

For a WPF event handler, async void is appropriate because event-handler signatures require void:

private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        customerList = await GetCustomersAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

For ordinary methods, prefer async Task rather than async void, so callers can await completion and handle exceptions.

If synchronous access is unavoidable at a true synchronous boundary, use:

Customers customers = GetCustomersAsync().GetAwaiter().GetResult();

However, do not use that on a WPF UI thread if the operation may capture the UI synchronization context. It can deadlock. It is most appropriate in a non-UI entry point or carefully designed infrastructure code.

Step by Step Execution

Consider this WPF-safe flow:

private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
    RefreshButton.IsEnabled = false;

    try
    {
        Customers customers = await GetCustomersAsync();
        customerList = customers;
    }
    finally
    {
        RefreshButton.IsEnabled = true;
    }
}

Step by step:

  1. The click handler starts on the WPF UI thread.
  2. The button is disabled immediately to prevent duplicate requests.
  3. GetCustomersAsync() starts the service operation and returns a Task<Customers>.
  4. await sees that the task is incomplete and returns control to the WPF dispatcher. The UI remains responsive.
  5. When the service task finishes, WPF resumes the continuation on the UI thread.
  6. The returned Customers value is assigned to customerList.
  7. The finally block enables the button whether the operation succeeded or threw an exception.

Compare that with blocking code:

Customers customers = GetCustomersAsync().Result;

The UI thread stops at .Result. If the async operation needs to resume on that same UI thread, it cannot complete.

Real World Use Cases

Async operations are common whenever an application waits for external work:

  • HTTP APIs: Download customer data without freezing a desktop window.
  • Databases: Query records while allowing the user to cancel or continue interacting with the UI.
  • Files: Read or write large files without blocking a server thread or UI thread.
  • Cloud services: Fetch data from storage, queues, or authentication providers.
  • Background processing: Process multiple independent requests efficiently on a web server.

In each case, await lets the current thread do other useful work instead of sitting idle.

Take Quiz

Real Codebase Usage

In production C# code, async behavior is usually propagated through layers:

public interface ICustomerService
{
    Task<Customers> GetCustomersAsync(CancellationToken cancellationToken);
}

public sealed class CustomerViewModel
{
    private readonly ICustomerService _customerService;

    public async Task RefreshAsync(CancellationToken cancellationToken)
    {
        customerList = await _customerService.GetCustomersAsync(cancellationToken);
    }
}

Common patterns include:

  • Async all the way: A UI event handler awaits a view-model method, which awaits a service method, which awaits HTTP or database work.
  • Cancellation: Accept a CancellationToken for requests that may outlive the screen or operation that started them.
  • Error handling: Use try/catch around await, where exceptions are naturally rethrown.
  • UI updates after await: In UI code, update controls or bound properties after the awaited operation completes.
  • Library code and ConfigureAwait(false): Reusable non-UI library code may use await task.ConfigureAwait(false) when it does not need the original context. This reduces context dependencies, but it is not a general fix for blocking UI code.
  • Synchronous boundaries: A console application's legacy Main, a legacy interface, or a migration point may need a synchronous bridge. Keep the bridge at the outermost boundary rather than scattering .Result throughout the application.

Common Mistakes

Blocking a UI thread

// Risky in WPF, WinForms, and similar UI contexts.
customerList = GetCustomersAsync().Result;

Avoid it by making the caller asynchronous and using await.

Using Wait() for a result

Task<Customers> task = GetCustomersAsync();
task.Wait();
Customers customers = task.Result;

Wait() blocks and wraps failures in an AggregateException. It also has the same UI deadlock risk. Prefer await.

Calling RunSynchronously() on an async task

Task<Customers> task = GetCustomersAsync();
task.RunSynchronously(); // Incorrect

RunSynchronously() only applies to a task explicitly created with an unstarted delegate. It does not run an already-created async operation synchronously.

Polling Task.Status

while (task.Status != TaskStatus.RanToCompletion)
{
}

This is busy waiting: it wastes CPU and does not correctly handle faulted or canceled tasks. Await the task instead.

Using async void for ordinary methods

public async void LoadCustomersAsync()
{
    customerList = await GetCustomersAsync();
}

Except for event handlers, return Task. That lets callers await the work and observe exceptions.

Creating overloads that differ only by return type

C# cannot distinguish methods using only the return type. These signatures cannot coexist:

public Task<Customers> GetCustomersAsync() => Service.GetCustomersAsync();
public async void GetCustomersAsync() { /* ... */ } // Invalid overload

Use distinct names based on intent, such as GetCustomersAsync for fetching data and LoadCustomersAsync for assigning it to UI state.

Pumping a nested dispatcher frame

Manually pumping the WPF dispatcher can introduce re-entrancy: unrelated UI work may execute while the original method is only partially complete. This can create difficult ordering bugs. Prefer a fully asynchronous design instead.

Comparisons

ApproachBlocks current thread?Suitable for WPF UI code?Exception behaviorTypical use
await taskNoYesOriginal exception is thrownDefault approach
task.ResultYesNo, deadlock riskOften wrapped in AggregateExceptionAvoid except controlled sync boundaries
task.Wait()YesNo, deadlock riskOften wrapped in AggregateExceptionAvoid for async results
task.GetAwaiter().GetResult()YesNo, deadlock riskOriginal exception is thrownRare outer synchronous boundary
task.RunSynchronously()Runs an unstarted delegate taskNo for existing async tasksDepends on taskRarely used; not a waiting mechanism

await differs from blocking because it schedules the rest of the method as a continuation and releases the current thread until the task completes.

Take Quiz

Cheat Sheet

  • Use Task<T> for async methods that return a value.
  • Name asynchronous methods with the Async suffix.
  • Prefer await GetCustomersAsync() over .Wait() or .Result.
  • Use async Task, not async void, except for event handlers.
  • Do not call .Result, .Wait(), or .GetAwaiter().GetResult() on a UI thread unless you can prove the task cannot require that context.
  • RunSynchronously() is not a way to synchronously wait for a task returned by an async method.
  • Do not poll Task.Status; await the task.
  • Catch exceptions around await:
try
{
    var customers = await GetCustomersAsync();
}
catch (HttpRequestException ex)
{
    // Handle a network failure.
}
  • At a carefully controlled synchronous boundary only:
var customers = GetCustomersAsync().GetAwaiter().GetResult();

Use this sparingly and never as a routine replacement for await.

FAQ

Can I call an async method synchronously in C#?

Yes, task.GetAwaiter().GetResult() can bridge to synchronous code, but it blocks a thread. In UI applications it can deadlock, so await is usually the correct solution.

Why does .Result deadlock in WPF?

WPF async continuations commonly need the dispatcher thread. .Result blocks that thread, so the continuation cannot run and the task cannot finish.

Why does RunSynchronously() throw an exception?

A task returned by an async method is not an unstarted task bound to a delegate. RunSynchronously() only works with specific manually created tasks, not with existing async operations.

Should I use Task.Run to avoid a deadlock?

Usually no. Wrapping async I/O in Task.Run is not a replacement for async design and consumes a thread-pool thread. Fix the caller to use await where possible.

Is async void ever correct?

Yes, for UI event handlers and other APIs that require a void callback. For regular application methods, return Task or Task<T>.

Does ConfigureAwait(false) solve all deadlocks?

No. It can help reusable library code avoid resuming on a captured context, but callers should still avoid synchronously blocking asynchronous work.

How do I get the result from Task<Customers>?

Inside async code, use:

Customers customers = await GetCustomersAsync();

This is the safe, idiomatic approach.

Take Quiz

Mini Project

Description

Build a WPF-style refresh workflow that requests customer data asynchronously, keeps the interface responsive, prevents duplicate refreshes, and shows an error when the request fails. This demonstrates why awaiting a task is safer than blocking it.

Goal

Implement an asynchronous customer refresh method that safely updates UI-related state after customer data is loaded.

Requirements

Use a method named GetCustomersAsync that returns Task<Customers>.|Create a RefreshAsync method that awaits the customer request.|Prevent a second refresh while the first request is running.|Handle request failures with try/catch.|Restore the refresh state even if the request fails.

Take Quiz

Keep learning

Related questions

View more