async
await
List````<T>````
C#
asynchronous programming

async and await while adding elements to ListT

Master System Design with Codemia

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

Introduction

Using async and await while building a List<T> is common when values come from HTTP calls, databases, or file I/O. The important design question is not whether you can add to a list asynchronously, but whether the work should be sequential or concurrent and who owns the mutation of the list.

Sequential Await Keeps Ordering Simple

If each operation depends on the previous one, or if strict order matters, await each task and add the result afterward.

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task<string> FetchItemAsync(int id)
8    {
9        await Task.Delay(100);
10        return $"item-{id}";
11    }
12
13    static async Task Main()
14    {
15        var items = new List<string>();
16
17        for (int i = 1; i <= 5; i++)
18        {
19            string value = await FetchItemAsync(i);
20            items.Add(value);
21        }
22
23        Console.WriteLine(string.Join(", ", items));
24    }
25}

This is often the best answer when correctness and readability matter more than maximum concurrency.

Use Task.WhenAll for Independent Work

If the operations do not depend on each other, start them together and await them as a group.

csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading.Tasks;
5
6class Program
7{
8    static async Task<string> FetchItemAsync(int id)
9    {
10        await Task.Delay(100);
11        return $"item-{id}";
12    }
13
14    static async Task Main()
15    {
16        var tasks = Enumerable.Range(1, 5)
17            .Select(FetchItemAsync)
18            .ToList();
19
20        string[] results = await Task.WhenAll(tasks);
21        var items = new List<string>(results);
22
23        Console.WriteLine(string.Join(", ", items));
24    }
25}

This is usually the right pattern for independent I/O-bound work because it overlaps waiting time without forcing several tasks to mutate the same list concurrently.

List<T> Is Not Thread-Safe for Concurrent Mutation

If multiple tasks call Add at the same time, a normal List<T> can become inconsistent because it is not designed for concurrent writes.

If you truly must mutate a shared list from several concurrent contexts, synchronize access.

csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading.Tasks;
5
6class Program
7{
8    static readonly object Gate = new object();
9
10    static async Task Main()
11    {
12        var items = new List<int>();
13
14        var tasks = Enumerable.Range(1, 100).Select(async i =>
15        {
16            await Task.Delay(5);
17            lock (Gate)
18            {
19                items.Add(i);
20            }
21        });
22
23        await Task.WhenAll(tasks);
24        Console.WriteLine(items.Count);
25    }
26}

Even here, a cleaner design is often to collect results first and build the final list afterward.

Avoid List<T>.ForEach With Async Lambdas

This is a very common trap:

csharp
var items = new List<int> { 1, 2, 3 };
items.ForEach(async x => await Task.Delay(10));

That starts async work, but it does not give you a clean way to await completion through ForEach. A normal loop or Task.WhenAll is almost always clearer.

Decide on Failure and Cancellation Behavior Up Front

A single failed operation raises another design question: should the whole list build fail, or should successful results still be kept?

  • Sequential await makes fail-fast behavior obvious.
  • 'Task.WhenAll fails the combined await if any task fails.'
  • A custom result wrapper can preserve successes and failures together.

That policy choice often matters more than the raw syntax of await.

Common Pitfalls

A common mistake is letting multiple tasks add to the same List<T> without synchronization.

Another issue is using sequential awaits for fully independent operations and then paying unnecessary latency.

Developers also frequently use ForEach with async lambdas and assume the outer method waits for them. It does not.

Summary

  • Use sequential await when order matters or the work is dependent.
  • Use Task.WhenAll when operations are independent and can run concurrently.
  • Do not mutate a shared List<T> concurrently unless you synchronize access.
  • Prefer collecting results first rather than letting many tasks race to call Add.
  • Avoid async lambda patterns that hide unawaited work.

Course illustration
Course illustration

All Rights Reserved.