WithParsed(async swallows exceptions
Some examples of the use of WithParsed / WithParsedAsync with expected behavior
internal static class Program
{
private static void Main(string[] args)
{
Parser.Default.ParseArguments<Options>(args).WithParsed(_ => throw new Exception());
=> Unhandled exception. System.Exception: Exception of type 'System.Exception' was thrown. As Expected.
internal static class Program
{
private static async Task Main(string[] args)
{
await Parser.Default.ParseArguments<Options>(args).WithParsedAsync(_ => throw new Exception());
=> Unhandled exception. System.Exception: Exception of type 'System.Exception' was thrown. As Expected.
internal static class Program
{
private static async Task Main(string[] args)
{
await Parser.Default.ParseArguments<Options>(args).WithParsedAsync(async _ =>
{
await Task.CompletedTask; //just for demonstration
throw new Exception();
});
=> Unhandled exception. System.Exception: Exception of type 'System.Exception' was thrown. As Expected.
When using WithParsed with an async expresion in a synchrone context, it swallows the exception
internal static class Program
{
private static void Main(string[] args)
{
Parser.Default.ParseArguments<Options>(args).WithParsed(async _ =>
{
await Task.CompletedTask; //just for demonstration
throw new Exception();
});
=> Process finished with exit code 0. Should also notify about Unhandled exception.
No, not a bug in CommandLineParser. This is how async methods work, and that's why there is an WithParsedAsync method specifically for async callbacks. What you are attempting here is wrong and a bad(TM) idea.
Your problem stems from trying to treat an async method in the same fashion as ye'olde synchronous methods. This is never going to work...
Exceptions occuring in an async method bubble "upwards" to the original caller through the Task object returned by the async method. This means, to properly handle exceptions occuring in an async method, the caller of the async method has to handle the Task object returned by that method TOO. In other words, exceptions thrown in async methods only have a chance to reliably bubble "upwards" if the caller of the async method handles the Task object.
If you find this difficult to understand, look at this little program: (dotnetfiddle https://dotnetfiddle.net/qP2ozk)
class Program
{
static void Main(string[] args)
{
try
{
SomeAsyncMethod();
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception!!! {ex.GetType().FullName}");
return;
}
Console.WriteLine("Yupp, as expected, no exception caught");
}
static async Task SomeAsyncMethod()
{
await Task.CompletedTask; //just for demonstration
throw new Exception();
}
}
If you execute this, you'll notice that no exception is being caught. Why? Because the caller of SomeAsyncMethod - the Main method - does NOT handle the Task object returned by SomeAsyncMethod (Main does not employ the await keyword -- which under the hood deals with the returned Task object, nor does Main handle the returned Task object explicitly).
Now, WithParsed obviously doesn't handle Task objects, because it is not meant to be used with async methods but rather with Action<T>-like delegate types.
And that's what WithParsedAsync is for -- handling the Task objects returned by an async callback. You will notice that the callback delegate type for WithParsedAsync is different from that of WithParsed - it is a Func<Task>-like delegate type here. You will also notice that WithParsedAsync itself returns a Task object, again because it is an async method itself, and thus allowing any possible exception occuring to further "bubble" upwards in the call chain.
There is another related reason why there are both WithParsed and WithParsedAsync, and not just a single WithParsed .You might have already realized that WithParsed will not be able to deal with async methods, unless the action-like delegate type would change into some Func<Task>-like delegate type. But then you have turned WithParsed into a pointless copycat of WithParsedAsync, and -- crucially and more importantly -- lost the ability to use Action<T>-like delegates/callback methods.