15 minlesson

Returning Values from Async Methods

Returning Values from Async Methods

Understanding how to properly return values from async methods, including when to use ValueTask and why async void should be avoided.

Task Return Type

The standard way to return values from async methods:

csharp
1async Task<string> GetUserNameAsync(int userId)
2{
3 var user = await database.GetUserAsync(userId);
4 return user.Name; // Returns string, wrapped in Task<string>
5}
6
7// Calling code
8string name = await GetUserNameAsync(123);

The return statement returns the unwrapped value - the compiler handles wrapping it in a Task.

Multiple Return Points

Async methods can have multiple return statements:

csharp
1async Task<int> GetValueAsync(bool condition)
2{
3 if (condition)
4 {
5 await Task.Delay(100);
6 return 42;
7 }
8
9 await Task.Delay(200);
10 return 0;
11}

Returning Without Async

Sometimes you already have a completed value:

csharp
1// Cached value - no async needed
2Task<string> GetCachedValueAsync(string key)
3{
4 if (_cache.TryGetValue(key, out var value))
5 {
6 return Task.FromResult(value); // No async/await needed
7 }
8
9 return FetchValueAsync(key); // Delegate to async method
10}

ValueTask for Performance

ValueTask<T> avoids heap allocation when the result is often synchronous:

csharp
1// Standard Task<T> - always allocates
2async Task<int> GetValueAsync()
3{
4 if (_cache.HasValue)
5 return _cache.Value; // Still allocates Task
6
7 return await ComputeValueAsync();
8}
9
10// ValueTask<T> - avoids allocation for sync path
11async ValueTask<int> GetValueAsync()
12{
13 if (_cache.HasValue)
14 return _cache.Value; // No allocation!
15
16 return await ComputeValueAsync();
17}

When to Use ValueTask

Use TaskUse ValueTask
Always async operationsOften completes synchronously
Methods called infrequentlyHot paths, called frequently
Result might be awaited multiple timesResult awaited exactly once
Simpler codePerformance-critical code

ValueTask Rules

csharp
1// RULE: Only await a ValueTask once
2var valueTask = GetValueAsync();
3var result = await valueTask;
4// await valueTask; // BUG! Can't await twice
5
6// RULE: Don't use .Result or .GetAwaiter() without checking
7if (valueTask.IsCompleted)
8{
9 var result = valueTask.Result; // OK - already complete
10}
11
12// RULE: Convert to Task if you need to await multiple times
13var task = valueTask.AsTask();
14await task;
15await task; // OK - Task can be awaited multiple times

async void - The Exception

async void methods are fire-and-forget - only use for event handlers:

csharp
1// Event handlers MUST be async void (required signature)
2async void Button_Click(object sender, EventArgs e)
3{
4 await ProcessClickAsync();
5}
6
7// Why async void is dangerous for regular methods:
8async void DangerousMethod()
9{
10 await Task.Delay(100);
11 throw new Exception("Oops!"); // Crashes the app!
12}
13
14// Can't catch exceptions:
15try
16{
17 DangerousMethod(); // Can't await
18}
19catch
20{
21 // Exception NOT caught here!
22}

async void Problems

  1. Exceptions crash the app - No way to observe/catch them
  2. Can't await - No way to know when it completes
  3. Testing is difficult - No Task to wait on
  4. Breaks error handling - Try/catch doesn't work
csharp
1// ALWAYS prefer Task over void:
2async Task SafeMethod()
3{
4 await Task.Delay(100);
5 throw new Exception("Oops!"); // Can be caught!
6}
7
8try
9{
10 await SafeMethod();
11}
12catch (Exception ex)
13{
14 // Exception IS caught here!
15}

Tuples for Multiple Values

Return multiple values using tuples:

csharp
1async Task<(string Name, int Age)> GetPersonAsync(int id)
2{
3 var person = await database.GetPersonAsync(id);
4 return (person.Name, person.Age);
5}
6
7// Deconstruct the result
8var (name, age) = await GetPersonAsync(123);
9Console.WriteLine($"{name} is {age} years old");
10
11// Or access as properties
12var result = await GetPersonAsync(123);
13Console.WriteLine($"{result.Name} is {result.Age} years old");

Early Return Pattern

Return early for validation or cached values:

csharp
1async Task<User> GetUserAsync(int id)
2{
3 // Early return for invalid input
4 if (id <= 0)
5 {
6 return null; // No await needed
7 }
8
9 // Early return for cached value
10 if (_cache.TryGet(id, out var cached))
11 {
12 return cached; // No await needed
13 }
14
15 // Actual async work
16 var user = await database.GetUserAsync(id);
17 _cache.Set(id, user);
18 return user;
19}

Summary Table

Return TypeUse Case
TaskAsync method with no return value
Task<T>Async method returning type T
ValueTask<T>Performance-critical, often synchronous
voidRegular sync methods only
async voidEvent handlers only

Key Takeaways

  • Use Task<T> as the default for async methods with return values
  • Use ValueTask<T> for hot paths that often complete synchronously
  • Never use async void except for event handlers
  • Use Task.FromResult() when you already have the value
  • Return tuples for multiple values
  • Early returns work naturally in async methods