C#’s WhenAll and Exception Handling
WhenAll method helps save time when processing lists of tasks. When thinking about exceptions, I couldn’t find good patterns that allowed me to access the full list of tasks after they’ve completed. This post outlines the solution I came up with, which hopefully will help you out too.
The Benefits of WhenAll
Lets backup and give a brief introduction to why
WhenAll is so useful. Say you have a list of tasks, how might you go about running them all? A naive approach would be to use a for loop and await them all.
Here’s is a simple interface that defines a method, then an implementation that takes 3 seconds to complete, simulating some sort of I/O request.
If I had 4 of these such tasks, I could get the results using a for loop.
What’s wrong with this approach?
Since I’m processing these pings one at a time inside of our for loop, this method takes ~12 seconds to complete. (3 + 3 + 3 + 3 = 12).
A much better approach would be to use
WhenAll to start them all at the same time, then process them whenever they all are finished.
Notice how I’m calling
Ping outside of my for loop, which starts all these tasks at the same time. Then the call to
WhenAll will wait until they’re all finished, then process each in my for loop.
Since all tasks start at the same time, this method only takes ~3 seconds to complete. Significantly speeding up this code.
If all you take away from this article is to not await things inside a for each loop, that’s great. But another problem now exists, what happens if one of our tasks throws an exception?
Here’s a new ping type that simulates some failure.
Lets also change our pingTasks to include two of these
Now what happens to my code? The
WhenAll call still waits for everything to complete, but now there’s an exception to deal with. Since there’s no error mechanism to catch an exception the exception goes up the call stack.
What happens if you introduce a try catch around the
That only catches the first exception. But what about all of our tasks? We had a total of four tasks, 2 working and 2 broken.
As far as I can tell,
WhenAll only throws the first exception. If you had 100 tasks and 1 of them threw an exception, you don’t have access to the 99 tasks that completed successfully.
Which is a bit annoying, but I found a fairly simple work around.
Buffering Your Method Call
Lets introduce a new method whose sole responsibility is to catch exceptions for individual tasks.
Then using this
BufferCall method in our original method.
BufferCall method makes the Ping call and catches any exceptions for an individual ping.
This helps you in two ways:
- Gives you the ability to specify what the default should be in case of failure. In this case we returned false.
- It also gives you access to the tasks that completed successfully. As far as I can tell, in a normal WhenAll you just lose access to everything else when an exception occurs.
Depending on your situation and what you’re computing, those are both really useful.
Writing non-blocking code is important! C# helps make that easier, but sometimes you find out things like
WhenAll only raises the first exception and have to implement something custom.
If you have any thoughts or know a better way to work with WhenAll, I’d love to hear them.