I am trying to throw and catch an AggregateException. I did not use exceptions very much on C#, but the behaviour I found is a little bit surprising.
My code is:
var numbers = Enumerable.Range(0, 20);
try
{
var parallelResult = numbers.AsParallel()
.Where(i => IsEven(i));
parallelResult.ForAll(e => Console.WriteLine(e));
}
catch (AggregateException e)
{
Console.WriteLine("There was {0} exceptions", e.InnerExceptions.Count());
}
It is calling the function IsEven
private static bool IsEven(int i)
{
if (i % 10 == 0)
throw new AggregateException("i");
return i % 2 == 0;
}
That throws the AggregateException.
I would expect the code to write every even number in the 0,20 range and "There was 1 exceptions" twice.
What I get is some numbers printed (they are random cause of ForAll) and then the exception is thrown, but not catched and the programs stop.
Am i missing something?
When using await, it's going to unwrap the first exception and return it, that's why we don't hit the catch (AggregateException e) line. But if we use something like the below code sample, we catch the AggregateException , note that it's not a good idea since we're blocking the running thread.
To avoid having to iterate over nested AggregateException exceptions, you can use the Flatten method to remove all the nested AggregateException exceptions, so that the InnerExceptions property of the returned AggregateException object contains the original exceptions.
AggregateException is used to consolidate multiple failures into a single, throwable exception object. It is used extensively in the Task Parallel Library (TPL) and Parallel LINQ (PLINQ). For more information, see Exception Handling and How to: Handle Exceptions in a PLINQ Query.
Turn on Break on All Exceptions (Debug, Exceptions) and rerun the program. This will show you the original exception when it was thrown in the first place. (comment appended): In VS2015 (or above). Select Debug > Options > Debugging > General and unselect the "Enable Just My Code" option.
This is actually kind of interesting. I think the problem is that you're using AggregateException
in an unexpected way, which is causing an error inside the PLINQ code.
The entire point of AggregateException
is to group together multiple exceptions that may occur simultaneously (or nearly so) in a parallel process. So AggregateException
is expected to have at least one inner exception. But you're throwing new AggregateException("i")
, which has no inner exceptions. The PLINQ code tries to examine the InnerExceptions
property, hits some sort of error (probably a NullPointerException
) and then it seems to go into a loop of some sort. This is arguably a bug in PLINQ, since you're using a valid constructor for AggregateException
, even if it is an unusual one.
As pointed out elsewhere, throwing ArgumentException
would be more semantically correct. But you can get the behavior you're looking for by throwing a correctly-constructed AggregateException
, for example by changing the IsEven
function to something like this:
private static bool IsEven(int i) { if (i % 10 == 0){ //This is still weird //You shouldn't do this. Just throw the ArgumentException. throw new AggregateException(new ArgumentException("I hate multiples of 10")); } return i % 2 == 0; }
I think the moral of the story is to not throw AggregateException
unless you really know exactly what you're doing, particularly if you're already inside a parallel or Task
-based operation of some kind.
I agree with others: this is a bug in .Net and you should report it.
The cause is in the method QueryEnd()
in the internal class QueryTaskGroupState
. Its decompiled (and slightly modified for clarity) code looks like this:
try { this.m_rootTask.Wait(); } catch (AggregateException ex) { AggregateException aggregateException = ex.Flatten(); bool cacellation = true; for (int i = 0; i < aggregateException.InnerExceptions.Count; ++i) { var canceledException = aggregateException.InnerExceptions[i] as OperationCanceledException; if (IsCancellation(canceledException)) { cacellation = false; break; } } if (!cacellation) throw aggregateException; } finally { this.m_rootTask.Dispose(); } if (!this.m_cancellationState.MergedCancellationToken.IsCancellationRequested) return; if (!this.m_cancellationState.TopLevelDisposedFlag.Value) CancellationState.ThrowWithStandardMessageIfCanceled( this.m_cancellationState.ExternalCancellationToken); if (!userInitiatedDispose) throw new ObjectDisposedException( "enumerator", "The query enumerator has been disposed.");
Basically, what this does is:
AggregateException
if it contains any non-cancellation exceptionsObjectDisposedException
for some reason (assuming userInitiatedDispose
is false
, which it is)So, if you throw an AggregateException
with no inner exceptions, ex
will be an AggregateException
containing your empty AggregateExcaption
. Calling Flatten()
will turn that into just an empty AggreateException
, which means it doesn't contain any non-cancellation exception, so the first part of the code thinks this is cancellation and doesn't throw.
But the second part of the code realizes this isn't cancellation, so it throws a completely bogus exception.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With