Question
Under which circumstances should an asynchronous C# method return Task rather than void?
public async Task AsyncMethod(int num)
instead of:
public async void AsyncMethod(int num)
Is returning Task mainly useful for tracking an operation's progress?
Also, are the async and await keywords unnecessary in this method?
public static async void AsyncMethod2(int num)
{
await Task.Factory.StartNew(() => Thread.Sleep(num));
}
Short Answer
You will learn why asynchronous C# methods should normally return Task, how callers await completion and catch failures, and the narrow case where async void is appropriate. You will also see why Task.Delay is a better choice than starting a thread only to sleep.
Concept
An async method begins work that may finish later. Its return type determines whether the caller receives a handle for that work.
async Taskreturns aTaskobject. The caller canawaitit, observe completion, catch exceptions, compose it with other tasks, or cancel it when the API supports cancellation.async voidreturns no handle. The caller cannot await it and generally cannot know when it has completed or failed.
For that reason, the usual rule is:
Return
Taskfrom asynchronous methods. Useasync voidonly for event handlers and similar framework-required callback signatures.
A Task does not automatically provide progress. Progress is usually reported separately with IProgress<T>, such as IProgress<int>. A Task represents completion, failure, and (when designed into the method) cancellation.
async void is special because exceptions do not get stored on a task for the caller to observe. In many application types, an exception escaping an async void method is raised through the current synchronization context and can terminate the application.
Mental Model
Think of an asynchronous operation as sending a package.
- A
Taskis the tracking receipt. You can wait for delivery, learn whether delivery failed, and coordinate it with other deliveries. voidis sending the package without a receipt. The operation may still happen, but the caller has no direct way to wait for it or handle a delivery failure.
An event handler is like a doorbell button: the framework presses it and does not expect a receipt back. That is why event handlers commonly use async void.
Syntax and Examples
The standard asynchronous method signature returns Task:
public async Task SaveProfileAsync(Profile profile)
{
await _profileRepository.SaveAsync(profile);
}
A caller can wait for it and handle errors:
try
{
await SaveProfileAsync(profile);
Console.WriteLine("Profile saved.");
}
catch (Exception ex)
{
Console.WriteLine($"Save failed: {ex.Message}");
}
If a method produces a value, return Task<T>:
public async Task<string> GetGreetingAsync(string name)
{
await Task.Delay(100);
return $"Hello, {name}!";
}
string greeting = await GetGreetingAsync("Mina");
Use async void for an event handler when the event delegate requires :
Step by Step Execution
Consider this method and caller:
public static async Task WaitAndPrintAsync(int milliseconds)
{
await Task.Delay(milliseconds);
Console.WriteLine("Finished waiting.");
}
public static async Task Main()
{
Console.WriteLine("Before");
await WaitAndPrintAsync(500);
Console.WriteLine("After");
}
Execution proceeds as follows:
MainwritesBefore.MaincallsWaitAndPrintAsync, which starts executing.Task.Delay(500)creates a task that completes in about 500 milliseconds.awaitsees that the delay task is incomplete, soWaitAndPrintAsyncreturns its incompleteTasktoMain.Mainawaits that task, so it does not run yet.
Real World Use Cases
Task-returning methods are used throughout real applications:
- HTTP APIs: await an API request before reading its response.
- Database access: await an insert, update, or query before using the result.
- File operations: await reading or writing a file without blocking a thread.
- Authentication: await token acquisition before calling a protected endpoint.
- Background workflows: await several operations with
Task.WhenAll. - Retry logic: await each attempt and delay between attempts.
async void is usually limited to framework entry points such as:
- UI button click handlers in WPF, WinForms, or similar UI frameworks.
- Other event handlers whose delegate returns
void.
Even in a UI event handler, move the actual work into a separate Task-returning method whenever possible.
Real Codebase Usage
A common design is to keep asynchronous business logic awaitable and use async void only at the application boundary.
private async void RefreshButton_Click(object? sender, EventArgs e)
{
try
{
RefreshButton.Enabled = false;
await RefreshOrdersAsync();
}
catch (HttpRequestException ex)
{
ShowError($"Could not refresh orders: {ex.Message}");
}
finally
{
RefreshButton.Enabled = true;
}
}
private async Task RefreshOrdersAsync()
{
var orders = await _orderClient.GetOrdersAsync();
_orderGrid.DataSource = orders;
}
This structure has useful properties:
RefreshOrdersAsynccan be unit-tested by awaiting it.- Other code can await it too.
- Exceptions can be handled at an appropriate boundary, such as the UI event handler.
- The UI can disable and re-enable controls in
try/finally.
For validation, use guard clauses before asynchronous work:
Common Mistakes
Using async void for ordinary methods
// Avoid for normal application logic.
public async void SaveAsync()
{
await _repository.SaveAsync();
}
The caller cannot await SaveAsync() or reliably catch its exception. Return Task instead:
public async Task SaveAsync()
{
await _repository.SaveAsync();
}
Assuming Task reports progress
A Task reports eventual completion, fault, or cancellation—not a percentage. Use IProgress<T> when progress is needed:
public async Task DownloadAsync(IProgress<int> progress)
{
progress.Report(50);
await Task.Delay(100);
progress.Report();
}
Comparisons
| Feature | async Task | async Task<T> | async void |
|---|---|---|---|
Caller can await it | Yes | Yes | No |
| Returns a result | No | Yes, T | No |
Caller can observe exceptions via await | Yes | Yes | No task is returned |
Can compose with Task.WhenAll | Yes | Yes | No |
| Suitable for reusable application logic |
Cheat Sheet
- Prefer
async Taskfor an async method with no result. - Prefer
async Task<T>for an async method that producesT. - Use
async voidonly when a required callback or event-handler signature returnsvoid. - A
Tasklets callersawait, compose, and observe exceptions. - A
Taskdoes not automatically report progress; useIProgress<T>. - Use
await Task.Delay(milliseconds)for an asynchronous delay. - Avoid
Task.Factory.StartNew(() => Thread.Sleep(...))for delays. - Avoid
.Wait()and.Resultin asynchronous flows; useawait. - If a method only returns another task and adds no work, it may not need
async:
public Task PauseAsync(int milliseconds) => Task.Delay(milliseconds);
FAQ
Should every async C# method return Task?
Nearly every ordinary asynchronous method should return Task or Task<T>. The main exception is an event handler or callback that is required to return void.
Why is async void dangerous?
The caller cannot await it, coordinate with its completion, or catch its exceptions through await. This makes testing and error handling harder.
Can I await an async void method?
No. It has no returned task to await. Change the method to return Task if you control its signature.
Does returning Task make a method run on another thread?
No. Task represents asynchronous work; it does not guarantee a new thread. Many operations, such as network I/O and Task.Delay, can wait without occupying a thread.
Is Task.Factory.StartNew needed to use Thread.Sleep asynchronously?
No, and it is not a good delay pattern. Use await Task.Delay(num) instead.
Mini Project
Description
Create a small simulated report generator. It validates input, waits asynchronously to represent report generation, reports progress, and returns the completed report text. This demonstrates why a reusable async operation should return Task<T> rather than void.
Goal
Build and await a report-generation method that returns a result and reports progress without blocking a thread.
Requirements
- Create a method named
GenerateReportAsyncthat returnsTask<string>. - Accept a report name and an
IProgress<int>parameter. - Reject an empty report name with
ArgumentException. - Report progress at more than one point during generation.
- Use
Task.Delayinstead ofThread.Sleep. - Await the method from
Mainand print the generated report.
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.