Where does an async Task throw Exception if it is

2019-04-06 16:50发布

问题:

I have the following example: (please also read comments in code, as it will make more sense )

public async Task<Task<Result>> MyAsyncMethod() 
{
    Task<Result> resultTask = await _mySender.PostAsync();
    return resultTask; 

    // in real-life case this returns to a different assembly which I can't change
   // but I need to do some exception handling on the Result in here
}

let's assume the PostAsync method of _mySender looks like this:

public Task<Task<Result>> PostAsync() 
{
  Task<Result> result = GetSomeTask(); 
  return result;
}

The question is:

As I don't await for the actual Result in the MyAsyncMethod and if PostAsync method throws an exception, in which context is the exception going to be thrown and handled?

and

Is there any way I can handle exceptions in my assembly?

I was surprised that when I tried to change MyAsyncMethod to:

 public async Task<Task<Result>> MyAsyncMethod() 
{
  try 
  {
    Task<Result> resultTask = await _mySender.PostAsync();
    return resultTask; 
  }
  catch (MyCustomException ex) 
  {
  } 
}

the exception was caught here, event if there's no await for the actual result. It happens that the result of PostAsync is already available and the exception is thrown in this context right??

Is it possible to use ContinueWith to handle exceptions in the current class? For example:

public async Task<Task<Result>> MyAsyncMethod() 
{
    Task<Result> resultTask = await _mySender.PostAsync();
    var exceptionHandlingTask = resultTask.ContinueWith(t => { handle(t.Exception)}, TaskContinuationOptions.OnlyOnFaulted);
    return resultTask;
}

回答1:

This is a lot of questions to pack into a single "question", but OK...

Where does an async Task throw Exception if it is not awaited?

Unobserved Task exceptions are raised by the TaskScheduler.UnobservedTaskException event. This event is raised "eventually" because the Task must actually be garbage collected before its exception is considered unhandled.

As I don't await for the actual Result in the MyAsyncMethod and if PostAsync method throws an exception, in which context is the exception going to be thrown and handled?

Any method that uses the async modifier and returns a Task will put all of its exceptions on that returned Task.

Is there any way I can handle exceptions in my assembly?

Yes, you could replace the returned task, something like:

async Task<Result> HandleExceptionsAsync(Task<Result> original)
{
  try
  {
    return await original;
  }
  catch ...
}

public async Task<Task<Result>> MyAsyncMethod()
{
  Task<Result> resultTask = await _mySender.PostAsync();
  return HandleExceptionsAsync(resultTask);
}

I was surprised that when I tried to change MyAsyncMethod to [synchronously return the inner task] the exception was caught here, even if there's no await for the actual result.

That actually means that the method you're calling is not async Task, as your code example shows. It's a non-async, Task-returning method, and when one of those methods throws an exception, it's treated just like any other exception (i.e., it passes directly up the call stack; it's not placed on the returned Task).

Is it possible to use ContinueWith to handle exceptions in the current class?

Yes, but await is cleaner.



回答2:

I use an extension method for generic error handling on Task. This provides a way to both log all errors and do a custom action if an error occurs.

public static async void ErrorHandle(this Task task, Action action = null)
{
    try
    {
        await task.ConfigureAwait(false);
    }
    catch (Exception e)
    {
        Log.Error(e);
        if (action != null)
            action();
    }
}

I tend to use it when I do a "fire and forget" Task:

Task.Run(() => ProcessData(token), token).ErrorHandle(OnError);


回答3:

As I don't await for the actual Result in the MyAsyncMethod and if PostAsync method throws an exception, in which context is the exception going to be thrown and handled?

If you dont await any of the tasks in your code or dont assign a continuation, behavior may differ depending on the .NET framework version you're using. In both cases, the returned Task will swallow the exception, the difference will occur at finalization:

  1. .NET 4.0 - The finalizer thread will rethrow the swallowed exception. If no global exception handler is registered it will terminate th process

  2. .NET 4.5 and above - The exception will be swallowed and will go unnoticed.

In both cases TaskScheduler.UnobservedTaskException event will trigger:

Occurs when a faulted task's unobserved exception is about to trigger exception escalation policy, which, by default, would terminate the process.

When an non async Task returning method executes synchronously the exception is propogated immediately and that is why you're catching the exception without using await, but you should definitely not be depending on that in your code.

Is there any way I can handle exceptions in my assembly

Yes, you can. I would advise you await on tasks you execute inside your assembly.

There is no reason to be using the async modifier if you aren't awaiting anything:

public Task<Result> PostAsync() 
{
     return GetSomeTask();
}

Then, you can await on PostAsync and catch the exception there:

public async Task<Result> MyAsyncMethod() 
{
    try
    {
         // No need to use Task<Result> as await will unwrap the outter task
         return await _mySender.PostAsync();
     }
     catch (MyCustomException e)
     {
           // handle here
     }
}

You can even modify this code further and remove the async keyword and possibly catch the exception even higher up the callstack to the method calling MyAsyncMethod.