Question

C# async Task vs async void: When to Use Each

csharpasynchronous.net-4.5

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 Task returns a Task object. The caller can await it, observe completion, catch exceptions, compose it with other tasks, or cancel it when the API supports cancellation.
  • async void returns 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 Task from asynchronous methods. Use async void only 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 Task is the tracking receipt. You can wait for delivery, learn whether delivery failed, and coordinate it with other deliveries.
  • void is 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.

Take Quiz

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 void:

private async void SaveButton_Click(object? sender, EventArgs e)
{
    try
    {
        await SaveProfileAsync(_currentProfile);
    }
    catch (Exception ex)
    {
        ShowError(ex.Message);
    }
}

The event handler is void because its required signature is defined by the event. The reusable SaveProfileAsync method still returns Task.

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:

  1. Main writes Before.
  2. Main calls WaitAndPrintAsync, which starts executing.
  3. Task.Delay(500) creates a task that completes in about 500 milliseconds.
  4. await sees that the delay task is incomplete, so WaitAndPrintAsync returns its incomplete Task to Main.
  5. Main awaits that task, so it does not run After yet.
  6. When the delay completes, WaitAndPrintAsync resumes and writes Finished waiting.
  7. Its returned task completes.
  8. Main resumes and writes After.

The output order is reliably:

Before
Finished waiting.
After

If WaitAndPrintAsync returned void, Main could not await it. After could run before the wait was finished.

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.

Take Quiz

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:

  • RefreshOrdersAsync can 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:

public async Task SendEmailAsync(string address, string message)
{
    if (string.IsNullOrWhiteSpace(address))
    {
        throw new ArgumentException("An email address is required.", nameof(address));
    }

    await _emailClient.SendAsync(address, message);
}

For independent operations, compose tasks rather than using async void methods:

await Task.WhenAll(
    LoadUsersAsync(),
    LoadProductsAsync(),
    LoadOrdersAsync());

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(100);
}

Blocking with .Wait() or .Result

// Can block a thread and may deadlock in some application types.
LoadDataAsync().Wait();

Prefer await:

await LoadDataAsync();

Using Thread.Sleep in asynchronous code

await Task.Factory.StartNew(() => Thread.Sleep(1000));

This consumes a thread-pool thread for one second just to wait. For a time delay, use:

await Task.Delay(1000);

Forgetting to handle exceptions in an event handler

Because event handlers are often async void, put await inside try/catch when you need to show or log failures.

Removing await without changing the method correctly

If a method contains no await, it often does not need async. But return the task directly only when that preserves the intended behavior:

public Task WaitAsync(int milliseconds)
{
    return Task.Delay(milliseconds);
}

Comparisons

Featureasync Taskasync Task<T>async void
Caller can await itYesYesNo
Returns a resultNoYes, TNo
Caller can observe exceptions via awaitYesYesNo task is returned
Can compose with Task.WhenAllYesYesNo
Suitable for reusable application logicYesYesUsually no
Suitable for void event handlersUsually use a helper method insteadUsually use a helper method insteadYes

Task.Delay and Thread.Sleep are also importantly different:

OperationTask.DelayThread.Sleep
Blocks the current threadNoYes
Works naturally with awaitYesNo
Best for asynchronous waitingYesNo
Typical useDelays, retry backoff, timeoutsRare synchronous/thread-specific code
Take Quiz

Cheat Sheet

  • Prefer async Task for an async method with no result.
  • Prefer async Task<T> for an async method that produces T.
  • Use async void only when a required callback or event-handler signature returns void.
  • A Task lets callers await, compose, and observe exceptions.
  • A Task does not automatically report progress; use IProgress<T>.
  • Use await Task.Delay(milliseconds) for an asynchronous delay.
  • Avoid Task.Factory.StartNew(() => Thread.Sleep(...)) for delays.
  • Avoid .Wait() and .Result in asynchronous flows; use await.
  • 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.

Are async and await unnecessary in AsyncMethod2?

They are necessary in its shown form because await is how the method asynchronously waits for the inner task. However, the method should normally return Task, and its implementation should be await Task.Delay(num).

Can I remove async when returning Task?

Yes, if the method simply returns an existing task and needs no await, try/catch, finally, or post-await logic:

public Task WaitAsync(int milliseconds) => Task.Delay(milliseconds);
Take Quiz

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 GenerateReportAsync that returns Task<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.Delay instead of Thread.Sleep.
  • Await the method from Main and print the generated report.
Take Quiz

Keep learning

Related questions

View more