1

Evening,

Can anyone point me to documentation for why the linq statement using an async and await operators will not propagate an exception up the stack but the foreach loop will? There has to be something under the hood and documented somewhere for this behavior, I am just having no luck finding it. #googlefoofail #doyouevenasyncbrah

If you run the unit test, you will see it come back with no errors and valid. But if you debug the unit test it will break on the error however it will allow you to continue and that error never propagates back up the stack.

If you comment out the linq statement and uncomment the foreach loop, that errors as expected when trying to add a duplicate key to the dictionary.

[TestMethod]
public void tester()
{
    List<KeyValuePair<string, string>> pairs = new List<KeyValuePair<string, string>>() 
    { 
        { new KeyValuePair<string, string>("test", "first") }, 
        { new KeyValuePair<string, string>("test", "second") } 
    };
    fails = new Dictionary<string, string>();

    pairs.ForEach(async x => await testasync(x));                //<========= does not throw error

    //foreach (KeyValuePair<string,string> pair in pairs)        //<========= throws error
    //{
    //    testasync(pair).GetAwaiter().GetResult();
    //}

    Assert.IsTrue(1 == 1);
}

private Dictionary<string, string> fails;
private async Task testasync(KeyValuePair<string, string> pair)
{
    fails.Add(pair.Key, pair.Value);
}
Andrew Hillier
  • 137
  • 1
  • 9
Adrian Hoffman
  • 183
  • 1
  • 8
  • Not sure about the documentation, but there is an extension method mentioned here https://stackoverflow.com/a/28996883/4149124 which may help you get the exception thrown. – Andrew Hillier Apr 23 '21 at 22:35

2 Answers2

6

Can anyone point me to documentation for why the linq statement using an async and await operators will not propagate an exception up the stack but the foreach loop will?

It's not a LINQ statement, actually; it's List<T>.ForEach. List<T>.ForEach doesn't work with async lambdas because it only takes a void-returning delegate type, not a Task-returning one. So the async lambda becomes an async void method, which should be avoided.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

Here's some good documentation to explain the behavior you're seeing. Key excerpt:

When a task that runs asynchronously throws an exception, that Task is faulted. The Task object holds the exception thrown in the Task.Exception property. Faulted tasks throw an exception when they're awaited.

Since you never awaited the result of your Lambda, the exception stored in the resulting Task was never thrown. You could do any of the following to fix that:

    // more equivalent to your `foreach` loop.
    pairs.ForEach(x => testasync(x).GetAwaiter().GetResult());
    Task.WhenAll(pairs.Select(async x => await testasync(x))).GetAwaiter().GetResult();
    // preferred
    await Task.WhenAll(pairs.Select(async x => await testasync(x)));

The last line is the best way, but in order to use await there you have to make the calling method async, which means you should change it to return a Task, and anybody who calls it should await the returned Task, and so on up the entire call chain. This is what people mean when they say "async all the way".

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315