Catch an exception thrown by an async void method
Solution 1
It's somewhat weird to read but yes, the exception will bubble up to the calling code - but only if you await
or Wait()
the call to Foo
.
public async Task Foo()
{
var x = await DoSomethingAsync();
}
public async void DoFoo()
{
try
{
await Foo();
}
catch (ProtocolException ex)
{
// The exception will be caught because you've awaited
// the call in an async method.
}
}
//or//
public void DoFoo()
{
try
{
Foo().Wait();
}
catch (ProtocolException ex)
{
/* The exception will be caught because you've
waited for the completion of the call. */
}
}
As Stephen Cleary wrote in Async/Await - Best Practices in Asynchronous Programming:
Async void methods have different error-handling semantics. When an exception is thrown out of an async Task or async Task method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started.
Note that using Wait()
may cause your application to block, if .NET decides to execute your method synchronously.
This explanation http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions is pretty good - it discusses the steps the compiler takes to achieve this magic.
Solution 2
The reason the exception is not caught is because the Foo() method has a void return type and so when await is called, it simply returns. As DoFoo() is not awaiting the completion of Foo, the exception handler cannot be used.
This opens up a simpler solution if you can change the method signatures - alter Foo()
so that it returns type Task
and then DoFoo()
can await Foo()
, as in this code:
public async Task Foo() {
var x = await DoSomethingThatThrows();
}
public async void DoFoo() {
try {
await Foo();
} catch (ProtocolException ex) {
// This will catch exceptions from DoSomethingThatThrows
}
}
Solution 3
Your code doesn't do what you might think it does. Async methods return immediately after the method begins waiting for the async result. It's insightful to use tracing in order to investigate how the code is actually behaving.
The code below does the following:
- Create 4 tasks
- Each task will asynchronously increment a number and return the incremented number
- When the async result has arrived it is traced.
static TypeHashes _type = new TypeHashes(typeof(Program));
private void Run()
{
TracerConfig.Reset("debugoutput");
using (Tracer t = new Tracer(_type, "Run"))
{
for (int i = 0; i < 4; i++)
{
DoSomeThingAsync(i);
}
}
Application.Run(); // Start window message pump to prevent termination
}
private async void DoSomeThingAsync(int i)
{
using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
{
t.Info("Hi in DoSomething {0}",i);
try
{
int result = await Calculate(i);
t.Info("Got async result: {0}", result);
}
catch (ArgumentException ex)
{
t.Error("Got argument exception: {0}", ex);
}
}
}
Task<int> Calculate(int i)
{
var t = new Task<int>(() =>
{
using (Tracer t2 = new Tracer(_type, "Calculate"))
{
if( i % 2 == 0 )
throw new ArgumentException(String.Format("Even argument {0}", i));
return i++;
}
});
t.Start();
return t;
}
When you observe the traces
22:25:12.649 02172/02820 { AsyncTest.Program.Run
22:25:12.656 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.657 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0
22:25:12.658 02172/05220 { AsyncTest.Program.Calculate
22:25:12.659 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.659 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1
22:25:12.660 02172/02756 { AsyncTest.Program.Calculate
22:25:12.662 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.662 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2
22:25:12.662 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.662 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3
22:25:12.664 02172/02756 } AsyncTest.Program.Calculate Duration 4ms
22:25:12.666 02172/02820 } AsyncTest.Program.Run Duration 17ms ---- Run has completed. The async methods are now scheduled on different threads.
22:25:12.667 02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1
22:25:12.667 02172/02756 } AsyncTest.Program.DoSomeThingAsync Duration 8ms
22:25:12.667 02172/02756 { AsyncTest.Program.Calculate
22:25:12.665 02172/05220 Exception AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
22:25:12.668 02172/02756 Exception AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
22:25:12.724 02172/05220 } AsyncTest.Program.Calculate Duration 66ms
22:25:12.724 02172/02756 } AsyncTest.Program.Calculate Duration 57ms
22:25:12.725 02172/05220 Error AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0
Server stack trace:
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()
at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()
at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106
22:25:12.725 02172/02756 Error AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2
Server stack trace:
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()
at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()
at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0
22:25:12.726 02172/05220 } AsyncTest.Program.DoSomeThingAsync Duration 70ms
22:25:12.726 02172/02756 } AsyncTest.Program.DoSomeThingAsync Duration 64ms
22:25:12.726 02172/05220 { AsyncTest.Program.Calculate
22:25:12.726 02172/05220 } AsyncTest.Program.Calculate Duration 0ms
22:25:12.726 02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3
22:25:12.726 02172/05220 } AsyncTest.Program.DoSomeThingAsync Duration 64ms
You will notice that the Run method completes on thread 2820 while only one child thread has finished (2756). If you put a try/catch around your await method you can "catch" the exception in the usual way although your code is executed on another thread when the calculation task has finished and your contiuation is executed.
The calculation method traces the thrown exception automatically because I did use the ApiChange.Api.dll from the ApiChange tool. Tracing and Reflector helps a lot to understand what is going on. To get rid of threading you can create your own versions of GetAwaiter BeginAwait and EndAwait and wrap not a task but e.g. a Lazy and trace inside your own extension methods. Then you will get much better understanding what the compiler and what the TPL does.
Now you see that there is no way to get in a try/catch your exception back since there is no stack frame left for any exception to propagate from. Your code might be doing something totally different after you did initiate the async operations. It might call Thread.Sleep or even terminate. As long as there is one foreground thread left your application will happily continue to execute asynchronous tasks.
You can handle the exception inside the async method after your asynchronous operation did finish and call back into the UI thread. The recommended way to do this is with TaskScheduler.FromSynchronizationContext. That does only work if you have an UI thread and it is not very busy with other things.
Solution 4
Its also important to note that you will lose the chronological stack trace of the exception if you you have a void return type on an async method. I would recommend returning Task as follows. Going to make debugging a whole lot easier.
public async Task DoFoo()
{
try
{
return await Foo();
}
catch (ProtocolException ex)
{
/* Exception with chronological stack trace */
}
}
Solution 5
The exception can be caught in the async function.
public async void Foo()
{
try
{
var x = await DoSomethingAsync();
/* Handle the result, but sometimes an exception might be thrown
For example, DoSomethingAsync get's data from the network
and the data is invalid... a ProtocolException might be thrown */
}
catch (ProtocolException ex)
{
/* The exception will be caught here */
}
}
public void DoFoo()
{
Foo();
}
Comments
-
jmistx almost 2 years
Using the async CTP from Microsoft for .NET, is it possible to catch an exception thrown by an async method in the calling method?
public async void Foo() { var x = await DoSomethingAsync(); /* Handle the result, but sometimes an exception might be thrown. For example, DoSomethingAsync gets data from the network and the data is invalid... a ProtocolException might be thrown. */ } public void DoFoo() { try { Foo(); } catch (ProtocolException ex) { /* The exception will never be caught. Instead when in debug mode, VS2010 will warn and continue. The deployed the app will simply crash. */ } }
So basically I want the exception from the async code to bubble up into my calling code if that is even possible at all.
-
svrist about 13 yearsDoes this give you any help? social.msdn.microsoft.com/Forums/en/async/thread/…
-
Mr Moose about 9 yearsIn case anyone stumbles on this in future, the Async/Await Best Practices... article has a good explanation of it in "Figure 2 Exceptions from an Async Void Method Can’t Be Caught with Catch". "When an exception is thrown out of an async Task or async Task<T> method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started."
-
Tselofan almost 7 yearsYou can use this approach or this
-
Marcos Pereira about 2 years@MrMoose What is a SynchronizationContext? What does it mean for an exception to be raised within it?
-
Mr Moose about 2 years@MarcosPereira, Unfortunately, I can't give you a solid answer other than that it is part of the System.Threading namespace. Take a look at the remarks section of the documentation and the link that it refers to for more information
-
-
jmistx about 13 yearsThnx for the quick reply, btw what do you mean it's weird, all suggestions are welcome, don't hold back , it's how I learn :p
-
Stuart about 13 yearsI actually mean it's straight-forward to read - whereas I know what's actually going on is really complicated - so my brain is telling me not to believe my eyes...
-
jmistx about 13 yearsHey, I know but I really need that information in DoFoo so I can display the information in the UI. In this case it's important for the UI to display the exception as it is not an end user tool but a tool to debug a communication protocol
-
Sanjeevakumar Hiremath about 13 yearsIn that case, callbacks make a lot of sense.(good old async delegates)
-
Eric J. about 13 years@Tim: Include whatever information you need in the thrown exception?
-
Theo Yaung about 13 yearsException propagation is one of my personal favorite parts of the design, in Async for C#/VB :-) Lambdas are nice for callbacks, but for chained async tasks, prior to C#/VB async there wasn't a clean way to unify exception handling. That's one of the really nice parts of baking it into the language.
-
mkamioner about 10 yearsThis was very helpful. The link you provided is dead though :-/
-
Sornii about 10 yearsI think the Foo() method should be marked as Task instead of void.
-
Stuart about 10 years@Sornii it's good practice to return
Task
- as then callers can choose to wait or await if they want to - but it's not essential. -
xanadont almost 10 yearsI'm pretty sure this will produce an AggregateException. As such, the catch block as appears in this answer will not catch the exception.
-
Stuart almost 10 years@xanadont generally I don't think it does - see explanations like - stiller.co.il/blog/2012/12/task-wait-vs-await - plus I just tested with this quick gist in linqpad - gist.github.com/slodge/b88e68c96dedef494181
-
xanadont almost 10 yearsAh, you're right, @Stuart. If you remove the exception handling from Foo() and place it into Main(), then it'll get wrapped as an AggregateException.
-
rism over 8 years"but only if you await or Wait() the call to Foo" How can you
await
the call to Foo, when Foo is returning void?async void Foo()
.Type void is not awaitable
? -
GGleGrand about 8 yearsThis can really sneak up on you and should be warned by the compiler.
-
Alexander Derck over 7 years@xanadont Thanks, that was the thing I didn't grasp yet :)
-
Hitesh P about 7 yearsCannot await void method, can it?
-
Tselofan almost 7 yearsIt's not possible await void method, or wait for it as
Foo().Wait();
This wrong or incomplete answer. -
Adam over 6 years"Except for a Main method in a console, which can't be async." Since C# 7.1, Main can now be an async method link
-
kudlatiger almost 6 yearswhat if my caller is on other project and dll referenced? does exception reach here?
-
Matias Grioni about 5 yearsThis will cause an issue with not all paths returning a value, since if there is an exception no value is returned, while in the try there is. If you have no
return
statement, this code works however, since theTask
is "implicitly" returned by usingasync / await
. -
Deepak almost 3 years@EricJ. The logic ends at the very start of await