C#
asynchronous programming
Task
async methods
duplicate

Return a Task instead of awaiting the inner method call

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, a method that only forwards another asynchronous call often does not need async and await at all. Returning the inner Task directly keeps the code shorter and avoids creating an unnecessary async state machine. The important caveat is that this is only safe when the wrapper truly adds no behavior of its own.

The Simple Pass-Through Case

These two methods look similar, but only one of them does unnecessary work.

csharp
1public Task<int> CountAsync()
2{
3    return _repository.CountAsync();
4}
csharp
1public async Task<int> CountAsync()
2{
3    return await _repository.CountAsync();
4}

If the wrapper is only forwarding the call, the first version is normally preferable. It expresses the intent directly and avoids extra machinery.

await Changes Behavior, Not Just Style

The reason this question matters is that await is not merely syntax decoration. It changes control flow, exception timing, and resource lifetime.

For example, consider error handling:

csharp
1public async Task<int> CountSafeAsync()
2{
3    try
4    {
5        return await _repository.CountAsync();
6    }
7    catch (TimeoutException)
8    {
9        return 0;
10    }
11}

If you rewrite this as return _repository.CountAsync();, the catch no longer handles asynchronous failures that occur after the method returns the task. That is a real behavior change.

Resource Lifetime Is Another Boundary

You usually need await when a resource must stay alive until the async operation finishes.

csharp
1public async Task<string> DownloadAsync(HttpClient client)
2{
3    using var response = await client.GetAsync("https://example.com");
4    return await response.Content.ReadAsStringAsync();
5}

This method must stay async. If you tried to return the inner task from inside the using scope, the response could be disposed before the read completed.

The same rule applies to:

  • 'using and await using'
  • 'try and finally'
  • transactions
  • locks or other scoped state

Keep await When the Wrapper Adds Logic

A wrapper should remain async if it transforms the result, validates input, logs completion, or performs several async steps.

csharp
1public async Task<string> GetUserNameAsync(int id)
2{
3    if (id <= 0)
4    {
5        throw new ArgumentOutOfRangeException(nameof(id));
6    }
7
8    var user = await _service.GetUserAsync(id);
9    return user.Name;
10}

This is not a pass-through method. It adds validation and result shaping, so await belongs there.

A Practical Rule of Thumb

Return the inner task directly when the method is only forwarding a call:

csharp
public Task SaveAsync(Document document) => _store.SaveAsync(document);

Keep await when you need any of the following:

  • exception handling around the asynchronous operation
  • resource lifetime that must extend until completion
  • result transformation
  • multiple asynchronous steps
  • completion-dependent logging or metrics

That rule is simple and usually enough for real code.

Performance Is Usually Secondary

Yes, removing an unnecessary async state machine can be a minor optimization. But in most application code, the more important benefit is clarity. A direct Task return says, very plainly, “this method is only forwarding the operation.”

Do not remove await mechanically across a codebase. Remove it where the wrapper is genuinely redundant.

Common Pitfalls

  • Returning the inner task from a method that relies on try and catch to handle async failures.
  • Returning a task from inside a using scope and disposing resources too early.
  • Treating this as a universal performance rule instead of checking whether the wrapper adds behavior.
  • Keeping async and await on methods that are pure pass-throughs.
  • Forgetting that stack traces and debugging behavior can look different between the two forms.

Summary

  • Return the inner Task directly when the method is a pure pass-through.
  • Keep await when the method handles exceptions, manages resources, or transforms results.
  • 'await changes behavior, not just syntax.'
  • The main benefit of direct task return is clearer intent, with a small performance benefit as a side effect.
  • Thin wrapper methods are the best candidates for this simplification.

Course illustration
Course illustration

All Rights Reserved.