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:
csharp1async Task<string> GetUserNameAsync(int userId)2{3 var user = await database.GetUserAsync(userId);4 return user.Name; // Returns string, wrapped in Task<string>5}67// Calling code8string 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:
csharp1async Task<int> GetValueAsync(bool condition)2{3 if (condition)4 {5 await Task.Delay(100);6 return 42;7 }89 await Task.Delay(200);10 return 0;11}
Returning Without Async
Sometimes you already have a completed value:
csharp1// Cached value - no async needed2Task<string> GetCachedValueAsync(string key)3{4 if (_cache.TryGetValue(key, out var value))5 {6 return Task.FromResult(value); // No async/await needed7 }89 return FetchValueAsync(key); // Delegate to async method10}
ValueTask for Performance
ValueTask<T> avoids heap allocation when the result is often synchronous:
csharp1// Standard Task<T> - always allocates2async Task<int> GetValueAsync()3{4 if (_cache.HasValue)5 return _cache.Value; // Still allocates Task67 return await ComputeValueAsync();8}910// ValueTask<T> - avoids allocation for sync path11async ValueTask<int> GetValueAsync()12{13 if (_cache.HasValue)14 return _cache.Value; // No allocation!1516 return await ComputeValueAsync();17}
When to Use ValueTask
| Use Task | Use ValueTask |
|---|---|
| Always async operations | Often completes synchronously |
| Methods called infrequently | Hot paths, called frequently |
| Result might be awaited multiple times | Result awaited exactly once |
| Simpler code | Performance-critical code |
ValueTask Rules
csharp1// RULE: Only await a ValueTask once2var valueTask = GetValueAsync();3var result = await valueTask;4// await valueTask; // BUG! Can't await twice56// RULE: Don't use .Result or .GetAwaiter() without checking7if (valueTask.IsCompleted)8{9 var result = valueTask.Result; // OK - already complete10}1112// RULE: Convert to Task if you need to await multiple times13var 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:
csharp1// Event handlers MUST be async void (required signature)2async void Button_Click(object sender, EventArgs e)3{4 await ProcessClickAsync();5}67// 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}1314// Can't catch exceptions:15try16{17 DangerousMethod(); // Can't await18}19catch20{21 // Exception NOT caught here!22}
async void Problems
- Exceptions crash the app - No way to observe/catch them
- Can't await - No way to know when it completes
- Testing is difficult - No Task to wait on
- Breaks error handling - Try/catch doesn't work
csharp1// ALWAYS prefer Task over void:2async Task SafeMethod()3{4 await Task.Delay(100);5 throw new Exception("Oops!"); // Can be caught!6}78try9{10 await SafeMethod();11}12catch (Exception ex)13{14 // Exception IS caught here!15}
Tuples for Multiple Values
Return multiple values using tuples:
csharp1async Task<(string Name, int Age)> GetPersonAsync(int id)2{3 var person = await database.GetPersonAsync(id);4 return (person.Name, person.Age);5}67// Deconstruct the result8var (name, age) = await GetPersonAsync(123);9Console.WriteLine($"{name} is {age} years old");1011// Or access as properties12var result = await GetPersonAsync(123);13Console.WriteLine($"{result.Name} is {result.Age} years old");
Early Return Pattern
Return early for validation or cached values:
csharp1async Task<User> GetUserAsync(int id)2{3 // Early return for invalid input4 if (id <= 0)5 {6 return null; // No await needed7 }89 // Early return for cached value10 if (_cache.TryGet(id, out var cached))11 {12 return cached; // No await needed13 }1415 // Actual async work16 var user = await database.GetUserAsync(id);17 _cache.Set(id, user);18 return user;19}
Summary Table
| Return Type | Use Case |
|---|---|
Task | Async method with no return value |
Task<T> | Async method returning type T |
ValueTask<T> | Performance-critical, often synchronous |
void | Regular sync methods only |
async void | Event 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 voidexcept for event handlers - Use
Task.FromResult()when you already have the value - Return tuples for multiple values
- Early returns work naturally in async methods