Our objective is to explore the answer to the question — “what happens when exceptions are thrown inside asynchronous operations?”
Some context before we start:
Asynchronous operations are anything that takes time to complete (like reading from files, opening DB connections) but we don’t have to wait for their completion to execute other unrelated code.
We are gonna look at TAP (Task based Asynchronous Pattern), which is the most commonly used way of handling asynchronous operations in C# currently. And more specifically, for the purpose of examples, we’ll look at asynchronous methods represented via async and await keywords since these are the ones we encounter most commonly (async — await is simply syntactic sugar where the C# compiler automatically creates and returns a Task, removing the burden from the programmer)
Let’s get into it!!
When an exception is thrown in an async-await method, the returned Task contains the exception thrown, in the Task.Exception property. This is an AggregateException and we can access all the exceptions that caused the failure from its InnerExceptions property. What’s important to understand in the above scenario is that if it’s an async-await method that throws an exception, we don’t have to handle the exception in calling code if we aren’t awaiting the task returned from the async-await method. The exception is internally handled and simply packaged inside the returned Task (which we can read to determine the cause of the exception).
This is in contrast to the scenario where an exception is thrown by an asynchronous method that manually returns a Task. In this case, the exception needs to be handled via a try-catch in the calling code otherwise it crashes the program, just like how exceptions behave with synchronous methods.
When an exception is thrown by an asynchronous method, the syntax to handle the exception in the calling code is the same as though the exception was thrown by a synchronous method. This is because of the support C# provides to exceptions for asynchronous programming. It does a lot of work in the background to keep the experience similar to synchronous behavior.
Some caveats are present though.
There’s a difference if we choose to do the below:
- call Wait()/Result
on the Task returned in the calling code, if there’s an exception thrown by an asynchronous method. This behavior is the same for both, an async-await asynchronous method and an asynchronous method that manually returns a Task. What differentiates the behavior here, is whether we choose to await or call Wait()/Result on the Task returned from the asynchronous method.
- If we await the Task returned, then the calling code needs to handle the first exception contained in AggregateException and not AggregateException itself.
2. And if we call Wait()/Result on the Task returned in the calling code, then we need to handle AggregateException. We can then read the InnerExceptions property of AggregateException and handle each exception as needed.
What happens when an exception is thrown in one or more Tasks which make up another Task?
A Task could be made up of one or more Tasks (via combinators like Task.WhenAny() or Task.WhenAll()). In this case, if an exception is thrown in one or more component Tasks, execution of the overall Task doesn’t stop. If it did, then some of the other component Tasks (where no exception occurred) would never be able to complete. Instead, the C# compiler internally handles these exceptions and packages them into an AggregateException. We can access the individual exceptions via the AggregateException’s InnerExceptions property.
One final thing to keep in mind is that, when an asynchronous method throws an exception, the status of its returned Task is set to Faulted.