async-await
Task.WaitAll
console-app
C#
return-value

How do I get a return value from Task.WaitAll in a console app?

Master System Design with Codemia

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

Introduction

Task.WaitAll waits for tasks to finish, but it does not return their results. That is because the method itself returns void. If you want values back, the tasks must be Task<T>, and you read the results from those task objects after they complete.

In modern C#, the better answer is usually await Task.WhenAll(...), which returns an array of results for Task<T> inputs and avoids blocking the current thread. WaitAll still works, but it is rarely the best API in new code.

Why Task.WaitAll Does Not Return Values

Consider this example:

csharp
1Task<int> task1 = Task.Run(() => 10);
2Task<int> task2 = Task.Run(() => 20);
3
4Task.WaitAll(task1, task2);
5
6int a = task1.Result;
7int b = task2.Result;
8Console.WriteLine(a + b);

WaitAll only guarantees completion. The results live on task1 and task2, because each Task<int> carries its own eventual value.

Use Task<T> and Read .Result After Waiting

If you are in a synchronous console app and must block, this is the basic pattern:

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static void Main()
7    {
8        Task<int> first = Task.Run(() => ComputeValue(5));
9        Task<int> second = Task.Run(() => ComputeValue(7));
10
11        Task.WaitAll(first, second);
12
13        int total = first.Result + second.Result;
14        Console.WriteLine($"Total: {total}");
15    }
16
17    static int ComputeValue(int input) => input * 2;
18}

Reading .Result after WaitAll is safe from a blocking standpoint because the tasks have already completed.

It is still worth remembering that this is synchronous waiting. Your console app may be fine with that, but the pattern does not scale as well as fully asynchronous composition.

Prefer Task.WhenAll in Modern Code

If you can use async Main, Task.WhenAll is cleaner because it gives you the values directly:

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static async Task Main()
7    {
8        Task<int> first = Task.Run(() => 10);
9        Task<int> second = Task.Run(() => 20);
10
11        int[] results = await Task.WhenAll(first, second);
12        Console.WriteLine(results[0] + results[1]);
13    }
14}

This avoids thread blocking and composes more naturally with other asynchronous code.

Aggregate Results from Many Tasks

For a dynamic number of tasks, keep them in a collection:

csharp
1using System;
2using System.Linq;
3using System.Threading.Tasks;
4
5Task<int>[] tasks = Enumerable.Range(1, 5)
6    .Select(i => Task.Run(() => i * i))
7    .ToArray();
8
9int[] values = await Task.WhenAll(tasks);
10int sum = values.Sum();
11
12Console.WriteLine(sum);

This is much cleaner than manually indexing each task after a WaitAll.

Handle Exceptions Correctly

Both WaitAll and WhenAll propagate exceptions, but the shape is slightly different. WaitAll throws an AggregateException, while await Task.WhenAll(...) unwraps in a way that is often easier to work with.

csharp
1try
2{
3    Task<int>[] tasks =
4    {
5        Task.Run(() => 10),
6        Task.Run<int>(() => throw new InvalidOperationException("Boom"))
7    };
8
9    await Task.WhenAll(tasks);
10}
11catch (Exception ex)
12{
13    Console.WriteLine(ex.Message);
14}

In real programs, exception behavior is another reason to favor await over blocking waits.

It also makes cancellation and timeout handling easier, because async code composes better with CancellationToken and other modern APIs.

Common Pitfalls

  • Expecting Task.WaitAll itself to return values. It never does.
  • Using Task instead of Task<T> when the operation should produce a result.
  • Calling .Result before completion, which can block unnecessarily.
  • Preferring blocking waits in code that could use async Main and await Task.WhenAll.
  • Ignoring exceptions from one task while reading results from the others. Completion does not imply success.

Summary

  • 'Task.WaitAll waits for completion but returns no values.'
  • Use Task<T> and read .Result from each task after waiting.
  • In new code, prefer await Task.WhenAll(...) because it returns the results directly.
  • Store many tasks in an array or list when aggregating values.
  • Handle exceptions deliberately, especially when several tasks run in parallel.

Course illustration
Course illustration

All Rights Reserved.