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?
Answer
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()
:
///
/// PumpingSyncContext, based on AsyncPump
/// http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx
///
class PumpingSyncContext : SynchronizationContext
{
BlockingCollection _actions;
int _pendingOps = 0;
public TResult Run(Func> taskFunc, CancellationToken token = default(CancellationToken))
{
_actions = new BlockingCollection();
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.
No comments:
Post a Comment