C#
asynchronous programming
async await
task management
.NET

C getting the results from an asynchronous 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#, the normal way to get the result from an asynchronous operation is to await a Task<T>. The difficulty is usually not the syntax, but knowing how to propagate async all the way up, combine multiple tasks, and avoid blocking calls such as .Result in environments that can deadlock or stall throughput.

The Basic Pattern: Task<T> Plus await

If an asynchronous method returns a value, its return type should usually be Task<T>. The caller then uses await to retrieve that value.

csharp
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4
5public static class Demo
6{
7    public static async Task<string> DownloadAsync(string url)
8    {
9        using var client = new HttpClient();
10        return await client.GetStringAsync(url);
11    }
12
13    public static async Task Main()
14    {
15        string html = await DownloadAsync("https://example.com");
16        Console.WriteLine(html.Length);
17    }
18}

This is the cleanest model because the runtime can resume the method when the task completes without blocking a thread just to wait.

Why await Is Better Than .Result

It is tempting to write:

csharp
string html = DownloadAsync("https://example.com").Result;

That blocks the calling thread. In UI applications and older ASP.NET request contexts, blocking can cause deadlocks or at least unnecessary thread starvation. Even when it does not deadlock, it defeats part of the benefit of async code.

The rule is simple: if a caller can be async, make it async.

Returning Results from Your Own Async Methods

You do not need extra wrapping to return a result from async methods. Just return the final value, and the compiler wraps it in a Task<T>.

csharp
1using System.Threading.Tasks;
2
3public static class MathService
4{
5    public static async Task<int> AddAfterDelayAsync(int a, int b)
6    {
7        await Task.Delay(200);
8        return a + b;
9    }
10}

Then consume it with await:

csharp
int sum = await MathService.AddAfterDelayAsync(2, 3);

Getting Results from Multiple Async Calls

If several calls are independent, start them first and then wait with Task.WhenAll.

csharp
1using System;
2using System.Threading.Tasks;
3
4public static class BatchDemo
5{
6    public static async Task<int> WorkAsync(int value)
7    {
8        await Task.Delay(100);
9        return value * 2;
10    }
11
12    public static async Task Main()
13    {
14        Task<int> t1 = WorkAsync(10);
15        Task<int> t2 = WorkAsync(20);
16        Task<int> t3 = WorkAsync(30);
17
18        int[] results = await Task.WhenAll(t1, t2, t3);
19        Console.WriteLine(string.Join(", ", results));
20    }
21}

This is usually better than awaiting each call one by one when they do not depend on each other.

What to Do in a Synchronous Boundary

Sometimes you are forced to call async code from a synchronous entry point you cannot change. In that case, the safest blocking form is usually:

csharp
string html = DownloadAsync("https://example.com").GetAwaiter().GetResult();

That still blocks, so it is not ideal. The main benefit is that it unwraps exceptions directly instead of wrapping them in AggregateException. Use it only at unavoidable boundaries, not as a normal coding style.

ASP.NET and Server Code Guidance

In ASP.NET applications, prefer making the controller, handler, or service method async instead of blocking on tasks in the request path.

csharp
1using System.Threading.Tasks;
2using System.Web.Mvc;
3
4public class HomeController : Controller
5{
6    public async Task<ActionResult> Index()
7    {
8        int value = await Task.FromResult(42);
9        ViewBag.Value = value;
10        return View();
11    }
12}

This keeps request threads available for other work and avoids the classic "async over sync over async" trap.

Exceptions Still Matter

Async results are not special with respect to errors. Exceptions are rethrown when you await the task.

csharp
1try
2{
3    string html = await DownloadAsync("https://invalid.example");
4    Console.WriteLine(html);
5}
6catch (HttpRequestException ex)
7{
8    Console.WriteLine(ex.Message);
9}

That means your error handling should stay near the await point where the result is actually consumed.

Common Pitfalls

The most common mistake is blocking with .Result or .Wait() in code that could have been async. Another is awaiting independent tasks sequentially and losing concurrency for no reason. Teams also sometimes write async methods that do not need to be async, or they wrap existing results in Task.Run even though no CPU-bound background work is needed. In server applications, mixing blocking waits and async request handlers can reduce throughput badly even when the code appears to work in development.

Summary

  • Use Task<T> and await to get results from asynchronous calls in C#.
  • Prefer async all the way up instead of blocking with .Result or .Wait().
  • Use Task.WhenAll when multiple independent async operations can run together.
  • Only use GetAwaiter().GetResult() at boundaries you truly cannot make async.
  • Keep exception handling around the await where the result is consumed.

Course illustration
Course illustration

All Rights Reserved.