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

async and await while adding elements to ListT

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

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.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.