Exceptions Active After Async Methods Have Been Caught

Now I know what to hack Marshal.GetExceptionCode()

in the first place, but that is not the question (Visual Studio debugger also detects an active exception)

private static async Task TestAsync()
{
    Log("TestAsync.Before");

    await HandleExceptionAsync();

    Log("TestAsync.After");
}

private static async Task HandleExceptionAsync()
{
    try
    {
        Log("HandleExceptionAsync.Try");
        await ThrowAsync();
    }
    catch (InvalidOperationException)
    {
        Log("HandleExceptionAsync.Catch");
    }

    Log("HandleExceptionAsync.AfterCatch");
}

private static async Task ThrowAsync()
{
    await Task.Delay(1000);
    throw new InvalidOperationException("Delayed exception");
}

private static void Log(string step)
{
    Console.WriteLine($"{step}: {Marshal.GetExceptionCode()}");
}

      

Output

TestAsync.Before: 0
HandleExceptionAsync.Try: 0
Exception thrown: 'System.InvalidOperationException' in Interactive.dll
Exception thrown: 'System.InvalidOperationException' in System.Private.CoreLib.ni.dll
HandleExceptionAsync.Catch: -532462766
HandleExceptionAsync.AfterCatch: -532462766
TestAsync.After: -532462766
The thread 9292 has exited with code 0 (0x0).

      

The exception remains active throughout the entire wait chain, even if it is caught. I have checked the generated code and it doesn't make it clear why this is happening, the relevant part (generated machine MoveNext

for HandleExceptionAsync

):

  void IAsyncStateMachine.MoveNext()
  {
    int num1 = this.\u003C\u003E1__state;
    try
    {
      if (num1 == 0)
        ;
      try
      {
        TaskAwaiter awaiter;
        int num2;
        if (num1 != 0)
        {
          Program.Log("HandleExceptionAsync.Try");
          awaiter = Program.ThrowAsync().GetAwaiter();
          if (!awaiter.IsCompleted)
          {
            this.\u003C\u003E1__state = num2 = 0;
            this.\u003C\u003Eu__1 = awaiter;
            Program.\u003CHandleExceptionAsync\u003Ed__1 stateMachine = this;
            this.\u003C\u003Et__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.\u003CHandleExceptionAsync\u003Ed__1>(ref awaiter, ref stateMachine);
            return;
          }
        }
        else
        {
          awaiter = this.\u003C\u003Eu__1;
          this.\u003C\u003Eu__1 = new TaskAwaiter();
          this.\u003C\u003E1__state = num2 = -1;
        }
        awaiter.GetResult();
        awaiter = new TaskAwaiter();
      }
      catch (InvalidOperationException ex)
      {
        Program.Log("HandleExceptionAsync.Catch");
      }
      Program.Log("HandleExceptionAsync.AfterCatch");
    }
    catch (Exception ex)
    {
      this.\u003C\u003E1__state = -2;
      this.\u003C\u003Et__builder.SetException(ex);
      return;
    }
    this.\u003C\u003E1__state = -2;
    this.\u003C\u003Et__builder.SetResult();
  }

      

I don't see that it has to do with the sync context (in this case it's a console app, so there are continuations scheduled in the pool), I think some call stack manipulation is happening, but I can't find any good information about it.

I would appreciate it if someone could explain why this is happening and provide a link to docs explaining how this is implemented in the CLR / compiler

UPD1: Added VS Debugger screenshots showing active exception in async showing nothing in sync

Asynchronous Repro inside VS 2015 debugger

Synchronize No exception in code sync version

+3


source to share


1 answer


If you put a breakpoint at Log("HandleExceptionAsync.AfterCatch");

, the column explains the trick:

ConsoleApp1.exe!ConsoleApp1.Program.Log(string step) Line 107   C#
ConsoleApp1.exe!ConsoleApp1.Program.HandleExceptionAsync() Line 95  C#
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext(object stateMachine)  Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.MoveNextRunner.Run()    Unknown
mscorlib.dll!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(System.Action action, bool allowInlining, ref System.Threading.Tasks.Task currentTask)    Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishContinuations()  Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageThree() Unknown
mscorlib.dll!System.Threading.Tasks.Task.FinishStageTwo()   Unknown
mscorlib.dll!System.Threading.Tasks.Task.Finish(bool bUserDelegateExecuted) Unknown
mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder<System.Threading.Tasks.VoidTaskResult>.SetException(System.Exception exception) Unknown
mscorlib.dll!System.Runtime.CompilerServices.AsyncTaskMethodBuilder.SetException(System.Exception exception)    Unknown
ConsoleApp1.exe!ConsoleApp1.Program.ThrowAsync() Line 101   C#
... (continues until the timer of Task.Delay)

      

See the bottom frame? We're still at ThrowAsync, even though we're logging in from HandleExceptionAsync

. How is this possible? The answer is also in the callstack:

mscorlib.dll!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject) Unknown

      

To put it simply, because of the keyword, await

your method HandleExceptionAsync

breaks like below:

void HandleExceptionAsync1()
{
    Log("HandleExceptionAsync.Try");
}

void HandleExceptionAsync2()
{
    Log("HandleExceptionAsync.AfterCatch");
}

      

Of course, this is much more difficult. In truth, the method is not interrupted and just transforms into a state machine. However, for this demo, it is reasonably equivalent.



HandleExceptionAsync2

must be done after ThrowAsync

. Therefore, it HandleExceptionAsync2

will be chained as a continuation. Something like:

ThrowAsync().ContinueWith(HandleExceptionAsync2);

      

(again, this is much more complicated than that . I'm just simplifying the explanation)

The "problem" is when the runtime completes the task returned by ThrowAsync:

System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetException(object exceptionObject)

      

The continuation will actually be embedded and executed in the same freeze frame (see frames above). This is an optimization often performed by the TPL for performance reasons. Because of this, when called Log("HandleExceptionAsync.AfterCatch");

, you are still in the catch blockThrowAsync

, hence the behavior you see.

+5


source







All Articles