Question
Run Async Task<T> Methods Synchronously in C# Without Deadlocks
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:
- Starts an async operation.
- Blocks with
.Wait(),.Result, or.GetAwaiter().GetResult(). - Prevents the UI thread from processing the continuation needed for the async operation to finish.
- 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.
awaitmeans: “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.
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:
- The click handler starts on the WPF UI thread.
- The button is disabled immediately to prevent duplicate requests.
GetCustomersAsync()starts the service operation and returns aTask<Customers>.awaitsees that the task is incomplete and returns control to the WPF dispatcher. The UI remains responsive.- When the service task finishes, WPF resumes the continuation on the UI thread.
- The returned
Customersvalue is assigned tocustomerList. - The
finallyblock 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.
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
CancellationTokenfor requests that may outlive the screen or operation that started them. - Error handling: Use
try/catcharoundawait, 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 useawait 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.Resultthroughout 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
| Approach | Blocks current thread? | Suitable for WPF UI code? | Exception behavior | Typical use |
|---|---|---|---|---|
await task | No | Yes | Original exception is thrown | Default approach |
task.Result | Yes | No, deadlock risk | Often wrapped in AggregateException | Avoid except controlled sync boundaries |
task.Wait() | Yes | No, deadlock risk | Often wrapped in AggregateException | Avoid for async results |
task.GetAwaiter().GetResult() | Yes | No, deadlock risk | Original exception is thrown | Rare outer synchronous boundary |
task.RunSynchronously() | Runs an unstarted delegate task | No for existing async tasks | Depends on task | Rarely 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.
Cheat Sheet
- Use
Task<T>for async methods that return a value. - Name asynchronous methods with the
Asyncsuffix. - Prefer
await GetCustomersAsync()over.Wait()or.Result. - Use
async Task, notasync 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.
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.
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.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.