-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggestion - Preserve all exceptions #7
Comments
This was an intentional design decision around This is nearly the worst of both worlds, too. Unlike try
{
await Task.WhenAll(x, y).PreserveAllExceptions();
}
catch (SomeException ex)
{
// Handle SomeException
}
catch (AggregateException aggregateEx) when (
aggregateEx.InnerExceptions.OfType<SomeException>().TrySingle(out var ex))
{
// Handle SomeException
// But don't forget to handle aggregateEx.InnerExceptions[0], too.
} It encourages catching AggregateException which leads you in the direction of the catch-all antipattern. For something top-level like a Cake script this would make sense. But for a structured application, it would typically force you to handle exceptions far too low, preventing the responsibility from being handled with the proper context higher up the call stack. /cc @SergeyTeplyakov |
I would be interested in the particulars of this. If the only impact of this is enabling a
Wrapping the exception is unnecessary overhead. Just use a custom struct awaiter based on |
Actually, isn't this the only way to ensure the correct behaviour. For example if we need to provide some compensating action when a try
{
var (a, b) = (
ThrowsArgumentNullException(),
ThrowsSqlException()
);
}
catch (SqlException ex)
{
// This will never be throw in this scenario.
// Even worse is that this behavior is dependent
// on the ordering of the tasks
} |
@michael-wolfenden But even in the scenario you're showing there, the other exception is an ArgumentNullException. A contract was invalidated; the program is fundamentally broken to an unknown degree and should fail fast. Therefore no compensating action would be needed for the SqlException. Or, let's say the first exception is an OperationCancelledException. Since you're in a wait-all, that means you definitely want to cancel without any results. Yet again, no compensating action. I'm just having a hard time thinking of a real-world business case to need to know about more than one of the exceptions. I've used async and WhenAll heavily and diagnosed many a confusing exception log; logging secondary errors can obscure the real problem. If the two exceptions truely are unrelated and have different root causes, you'll discover the second as soon as you fix the first. |
Catching all exception on the top level doesn't help much. The thing here is that in you example: try
{
var (a, b) = (
ThrowsArgumentNullException(),
ThrowsSqlException()
);
}
catch (SqlException ex)
{
// This will never be throw in this scenario.
// Even worse is that this behavior is dependent
// on the ordering of the tasks
} The In scenarios where you really care about handling all the exceptions it is better to use more flexible approach that would not only allow you to handle each exception in the place it should be but also to gather as much information as possible. Here is simple example of such approach: class ExceptionInfo
{
TaskMetadata Metadata { get; } // Task metadata also can be used to identify the Task
Exception Exception { get; }
}
class ExceptionTracker
{
public void Track(TaskMetadata t, Exception e) { ... }
public IReadOnlyList<ExceptionInfo> GetExceptions() { ... }
}
async Task Main()
{
var exceptionTracker = new ExceptionTracker();
await Task.WhenAll(A(exceptionTracker), B(exceptionTracker));
var exceptions = exceptionTracker.GetExceptions();
// analyze and decide what to do.
}
public async Task<int> A(ExceptionTracker exceptionTracker)
{
exceptionTracker.Track(new TaskMetadata(), new InvalidOperationException("A"));
}
public async Task<int> B(ExceptionTracker exceptionTracker)
{
exceptionTracker.Track(new TaskMetadata(), new InvalidOperationException("B"));
} |
Because of the pitfalls I mentioned above, we want to stay true to the C# language and BCL design of suppressing all but the first exception when a task is awaited. I like Sam's suggestion of a Thanks for the good discussion and I'm sorry I can't say yes. :-( |
As mention in the following tweet
An example is:
TaskTupleAwaiter
also exhibits the same behaviourSergey proposes the
PreserveAllExceptions
task extension, that can be chained onto theWhenAll
, i.e.await Task.WhenAll(A(), B()).PreserveAllExceptions();
that will wrap any originalAggregateException
s into another one if more then one error occurred.Would it be possible to add the
PreserveAllExceptions
method to all theWhenAll
calls made internally within this library so that all exceptions are preserved?I would be happy to submit a pull request.
The text was updated successfully, but these errors were encountered: