Question

I am using Steven Toub's excellent AsyncPump class that allows console applications to use the async/await keywords.

However, I have a problem where exceptions that are thrown in the code are caught by the pump and then rethrown, which causes the original call stack and exception context to be lost.

Here is my test code:

class Program
{
  static void Main(string[] arg)
  {
    AsyncPump.Run(() => MainAsync());
  }

  static async Task MainAsync()
  {
    throw new Exception(); // code should break here
  }
}

If you run this test, the debugger doesn't break on the throw new Exception() as desired. Instead, it breaks on t.GetAwaiter().GetResult(), which is part of the AsyncPump class itself. This makes debugging applications very difficult.

Is there any way to rethrow exceptions such that the debugger breaks at the original location while preserving the call stack and context?

Was it helpful?

Solution

You would probably see the desired behavior if you used async void signature for MainAsync, rather than async Task. This doesn't mean you should change your code (async void is almost never a good idea), it just means that the existing behavior is perfectly normal.

An exception thrown from async Task methods is not re-thrown immediately. Rather, it is stored inside the Task object (with the captured stack context) and will be re-thrown when the task's result gets observed via task.Result, task.Wait(), await task or task.GetAwaiter().GetResult().

I posted a bit more detailed explanation of this: TAP global exception handler.

On a side note, I use a slightly modified version of AsyncPump, which makes sure the initial task starts executing asynchronously (i.e., after the core loop has started pumping), with TaskScheduler.Current being TaskScheduler.FromCurrentSynchronizationContext():

/// <summary>
/// PumpingSyncContext, based on AsyncPump
/// http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
/// </summary>
class PumpingSyncContext : SynchronizationContext
{
    BlockingCollection<Action> _actions;
    int _pendingOps = 0;

    public TResult Run<TResult>(Func<Task<TResult>> taskFunc, CancellationToken token = default(CancellationToken))
    {
        _actions = new BlockingCollection<Action>();
        SynchronizationContext.SetSynchronizationContext(this);
        try
        {
            var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

            var task = Task.Factory.StartNew(
                async () =>
                {
                    OperationStarted();
                    try
                    {
                        return await taskFunc();
                    }
                    finally
                    {
                        OperationCompleted();
                    }
                },
                token, TaskCreationOptions.None, scheduler).Unwrap();

            // pumping loop
            foreach (var action in _actions.GetConsumingEnumerable())
                action();

            return task.GetAwaiter().GetResult();
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(null);
        }
    }

    void Complete()
    {
        _actions.CompleteAdding();
    }

    // SynchronizationContext methods
    public override SynchronizationContext CreateCopy()
    {
        return this;
    }

    public override void OperationStarted()
    {
        // called when async void method is invoked 
        Interlocked.Increment(ref _pendingOps);
    }

    public override void OperationCompleted()
    {
        // called when async void method completes 
        if (Interlocked.Decrement(ref _pendingOps) == 0)
            Complete();
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        _actions.Add(() => d(state));
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        throw new NotImplementedException("Send");
    }
}

It's also possible to change this part:

return task.GetAwaiter().GetResult();

To this:

return task.Result;

In this case, the exception will be propagated to the caller as AggregateException, with AggregateException.InnerException pointing to the original exception from inside the async method.

OTHER TIPS

GetAwaiter().GetResult() is already rethrowing exceptions properly (assuming you're on .NET 4.5). The call stack is properly preserved.

What you're observing is the behavior of a top-level exception being caught, and AFAIK it is strictly treated by VS as synchronous and there's no way to influence that. Sounds like it would make a good UserVoice item.

You do have the option of breaking when an exception is thrown.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top