Catch an exception thrown by an async void method

266,920

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();
}
Share:
266,920
jmistx
Author by

jmistx

Utterly Insane developer

Updated on July 17, 2022

Comments

  • jmistx
    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
      svrist about 13 years
    • Mr Moose
      Mr Moose about 9 years
      In 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
      Tselofan almost 7 years
      You can use this approach or this
    • Marcos Pereira
      Marcos Pereira about 2 years
      @MrMoose What is a SynchronizationContext? What does it mean for an exception to be raised within it?
    • Mr Moose
      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
    jmistx about 13 years
    Thnx 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
    Stuart about 13 years
    I 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
    jmistx about 13 years
    Hey, 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
    Sanjeevakumar Hiremath about 13 years
    In that case, callbacks make a lot of sense.(good old async delegates)
  • Eric J.
    Eric J. about 13 years
    @Tim: Include whatever information you need in the thrown exception?
  • Theo Yaung
    Theo Yaung about 13 years
    Exception 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
    mkamioner about 10 years
    This was very helpful. The link you provided is dead though :-/
  • Sornii
    Sornii about 10 years
    I think the Foo() method should be marked as Task instead of void.
  • Stuart
    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
    xanadont almost 10 years
    I'm pretty sure this will produce an AggregateException. As such, the catch block as appears in this answer will not catch the exception.
  • Stuart
    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
    xanadont almost 10 years
    Ah, 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
    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
    GGleGrand about 8 years
    This can really sneak up on you and should be warned by the compiler.
  • Alexander Derck
    Alexander Derck over 7 years
    @xanadont Thanks, that was the thing I didn't grasp yet :)
  • Hitesh P
    Hitesh P about 7 years
    Cannot await void method, can it?
  • Tselofan
    Tselofan almost 7 years
    It's not possible await void method, or wait for it as Foo().Wait(); This wrong or incomplete answer.
  • Adam
    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
    kudlatiger almost 6 years
    what if my caller is on other project and dll referenced? does exception reach here?
  • Matias Grioni
    Matias Grioni about 5 years
    This 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 the Task is "implicitly" returned by using async / await.
  • Deepak
    Deepak almost 3 years
    @EricJ. The logic ends at the very start of await