I was scrolling the Mastodon timeline when I noticed a fellow .NET developer, Bill Seipel, having an unexpected experience with List.ForEach and async/await. At first glance, I thought he was modifying the collection he was iterating over, but then I realized the issue was much more subtle.

In this post, we’ll discuss what’s happening in a recreation of his code and how you might fix it.

Let’s get started.

List.ForEach and Tasks lead to problems

For those unaware, the List type has a ForEach method, which allows the user to pass an Action<T>. The method passes the iteration’s item, allowing you to execute something similar to a foreach method. In a sense, it’s syntactic sugar. In another, it’s a relic before async/await. Let’s see an example code that can get you in trouble.

List<string> projects = new();  
List<string> measures = ["cm", "m", "km"];  
  
measures.ForEach(async x =>  
{  
     var result = await GetResultOfMeasure(x);  
     projects.Add(result);  
});  
  
Console.WriteLine(projects.Count);  
  
async Task<string> GetResultOfMeasure(string s)  
{  
     await Task.Delay(100);  
     return $"Measured as {s}";  
}

Looking at the code, we see an async/await, so what’s the problem? Well, let’s run the code and see what the result is.

0

What?! Why did we get a result of 0?

If you’re using a tool like ReSharper or JetBrains Rider you’ll have seen a warning around the async keyword.

Avoid using 'async' lambda when delegate type returns 'void'

Oops! While the Action uses async/await semantics, nothing awaits the iterative process. This means our iterations may or may not be complete by the time we reach the Console.WriteLine statement. Since we use Task.Delay, it’s likely we won’t.

If you’re using something that fluctuates in performance, you will likely pull out your hair due to indeterminate behavior.

So, what’s the fix?

Fixing List.ForEach with ForEachAsync

The most straightforward solution is not to use ForEach but to use a simple for each iteration instead.

List<string> projects = new();
List<string> measures = ["cm", "m", "km"];

foreach (var measure in measures)
{
     var result = await GetResultOfMeasure(measure);
     projects.Add(result);
}

Console.WriteLine(projects.Count);

But honestly, where’s the fun in that?! Another approach is to write a ForEachAsync extension method.

public static class ListExtension
{
     public static async Task ForEachAsync<T>(
          this List<T> collection, 
          Func<T, Task> action)
     {
          foreach (var i in collection) await action(i);
     }
}

From here, you can tweak the original sample code to be sure to await the entire iterative process.

List<string> projects = new();
List<string> measures = ["cm", "m", "km"];

await measures.ForEachAsync(async x =>
{
     var result = await GetResultOfMeasure(x);
     projects.Add(result);
});

Console.WriteLine(projects.Count);

async Task<string> GetResultOfMeasure(string s)
{
     await Task.Delay(100);
     return $"Measured as {s}";
}

There you have it. Pretty cool, right?!

Conclusion

This particular issue is insidious as it all compiles and generally looks “correct.” With good tooling, like that provided by JetBrains, you get a hint as to a potential issue lurking in your code, but it doesn’t scream at you since it’s still code that compiles. It might make sense for .NET to include a ForEachAsync method on List, but at this point, it might also make sense to Obsolete the method entirely since it likely does more harm than good.

What do you think? Let me know if you’ve ever run into this issue. You can see the full thread on Mastodon here, along with debugging videos describing what is happening.

Thanks for reading and sharing my work with friends and colleagues. Cheers.