Can I grab the original exception somehow and throw it instead?
"It" implies that there will only be on exception. Even though that's probably true, because you're executing actions in parallel you can't 100% rule out the possibility that multiple actions throw exceptions even if you attempt to cancel the others after the first exception. If you're okay with that, we can go from the assumption that we only expect one exception and we're okay with only catching one. (If you allow the other invocation to continue after one throws an exception the possibility of having two exceptions increases.)
You can use a cancellation token. If one of the invocations below throws an exception, it should catch that exception, place it in a variable or queue, and then call
source.Cancel;
Doing so will cause the entire Parallel.Invoke to throw an OperationCanceledException. You can catch that exception, retrieve the exception that was set, and rethrow that.
I'm going to go with the other answer's suggestion of a ConcurrentQueue just as a matter of practice because I don't think we can rule out the remote possibility that a second thread could throw an exception before being canceled.
This started off seeming small, but eventually it got so involved that I separated it into its own class. This makes me question whether my approach is needlessly complex. The main intent was to keep the messy cancellation logic from polluting your GetMaxRateDict and GetMinRateDict methods.
In addition to keeping your original methods unpolluted and testable, this class is itself testable.
I suppose I'll find out from the other responses whether this is a decent approach or there's something much simpler. I can't say I'm particularly excited about this solution. I just thought it was interesting and wanted to write something that did what you asked.
public class ParallelInvokesMultipleInvocationsAndThrowsOneException //names are hard { public void InvokeActions(params Action[] actions) { using (CancellationTokenSource source = new CancellationTokenSource()) { // The invocations can put their exceptions here. var exceptions = new ConcurrentQueue<Exception>(); var wrappedActions = actions .Select(action => new Action(() => InvokeAndCancelOthersOnException(action, source, exceptions))) .ToArray(); try { Parallel.Invoke(new ParallelOptions{CancellationToken = source.Token}, wrappedActions) } // if any of the invocations throw an exception, // the parallel invocation will get canceled and // throw an OperationCanceledException; catch (OperationCanceledException ex) { Exception invocationException; if (exceptions.TryDequeue(out invocationException)) { //rethrow however you wish. throw new Exception(ex.Message, invocationException); } // You shouldn't reach this point, but if you do, throw something else. // In the unlikely but possible event that you get more // than one exception, you'll lose all but one. } } } private void InvokeAndCancelOthersOnException(Action action, CancellationTokenSource cancellationTokenSource, ConcurrentQueue<Exception> exceptions) { // Try to invoke the action. If it throws an exception, // capture the exception and then cancel the entire Parallel.Invoke. try { action.Invoke(); } catch (Exception ex) { exceptions.Enqueue(ex); cancellationTokenSource.Cancel(); } } }
The usage would then be
var thingThatInvokes = new ParallelInvokesMultipleInvocationsAndThrowsOneException(); thingThatInvokes.InvokeActions( ()=> GetMaxRateDict(tradeOffObj), () => GetMinRateDict(tradeOffObj));
If it throws an exception, it will be a single exception from one invocation failure, not an aggregate exception.