asynchronous
task management
waiting
concurrency
programming

Waiting until the task finishes

Master System Design with Codemia

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

Introduction

Waiting for a task to finish sounds simple, but the right technique depends on whether your code is already asynchronous. In modern C#, the best answer is usually await, because it lets the task complete without blocking the thread that is running the caller.

await Is the Normal Solution

A Task represents work that may complete later. If the current method can be asynchronous, mark it async and use await. The caller pauses logically until the task is done, but the thread can be returned to the runtime instead of sitting idle.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        int result = await SlowAddAsync(3, 4);
9        Console.WriteLine(result);
10    }
11
12    static async Task<int> SlowAddAsync(int a, int b)
13    {
14        await Task.Delay(500);
15        return a + b;
16    }
17}

This is the most readable pattern because exceptions flow naturally and the intent is obvious.

Waiting for Several Tasks

Sometimes one task is not enough. If several operations can run in parallel, start them first and then wait for all of them together.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        Task<int> first = ComputeAsync(2);
9        Task<int> second = ComputeAsync(5);
10
11        int[] values = await Task.WhenAll(first, second);
12        Console.WriteLine(values[0] + values[1]);
13    }
14
15    static async Task<int> ComputeAsync(int value)
16    {
17        await Task.Delay(300);
18        return value * 10;
19    }
20}

Task.WhenAll is much better than awaiting each task immediately after creating it, because both tasks can make progress at the same time.

Blocking Waits and When to Avoid Them

Older code often uses .Wait() or .Result to block until a task finishes. That works in some console programs, but it is a poor default in GUI or ASP.NET request code because it can deadlock or waste a thread.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static void Main()
7    {
8        Task<int> task = SlowAddAsync(10, 20);
9        int result = task.GetAwaiter().GetResult();
10        Console.WriteLine(result);
11    }
12
13    static async Task<int> SlowAddAsync(int a, int b)
14    {
15        await Task.Delay(500);
16        return a + b;
17    }
18}

If you must bridge async code into a synchronous entry point, GetAwaiter().GetResult() is usually preferable to .Result because it throws the original exception instead of wrapping it in AggregateException. Even so, this should be a boundary technique, not your everyday pattern.

Knowing What You Are Really Waiting For

A task can finish in three ways: successful completion, faulted completion, or cancellation. Good waiting logic handles all three. That matters in real applications because timing out, canceling, and exception propagation are just as important as the success path.

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task Main()
8    {
9        using var cts = new CancellationTokenSource(200);
10
11        try
12        {
13            await WorkAsync(cts.Token);
14            Console.WriteLine("completed");
15        }
16        catch (OperationCanceledException)
17        {
18            Console.WriteLine("canceled");
19        }
20    }
21
22    static async Task WorkAsync(CancellationToken token)
23    {
24        await Task.Delay(1000, token);
25    }
26}

This pattern makes the waiting behavior explicit instead of assuming every task will eventually succeed.

Common Pitfalls

The biggest pitfall is blocking on async work from a context that expects asynchronous continuation, such as a UI thread. The code may freeze even though the task itself is fine.

Another problem is starting work and then waiting immediately, which removes any chance for concurrency. If you have independent operations, create the tasks first and await them together.

Exception handling is another source of confusion. If a task faults and nobody awaits it, the error can be delayed or observed in a surprising place. Always await the task you start unless you are intentionally using fire-and-forget behavior and have a separate error-handling strategy.

Finally, avoid mixing cancellation support into only half the pipeline. A caller may wait forever if the task ignores the token even though the outer code is prepared to cancel.

Summary

  • In modern C#, await is the preferred way to wait for a task to finish.
  • Use Task.WhenAll when several independent tasks can run in parallel.
  • Avoid .Wait() and .Result in UI and server code because they can block threads and cause deadlocks.
  • If synchronous code must wait, GetAwaiter().GetResult() is a safer boundary tool than .Result.
  • Handle cancellation and exceptions as part of task completion, not as afterthoughts.

Course illustration
Course illustration

All Rights Reserved.