Partager via


Async & exceptions in C#

This quick post is motivated by a question on StackOverflow. Basically it is a simple console program you can run to see how exceptions are handled in C# async methods. Common wisdom is "don't have async void methods; always return a Task" but that simple signature change is neither necessary nor sufficient to handle exceptions correctly.

Basically, in order to be safe you need to do one of two things:

  1. Handle exceptions within the async method itself; or
  2. Return a Task<T> and ensure that the caller attempts to get the result whilst also handling exceptions (possibly in a parent stack frame)

Failure to do either of these things will result in unwanted behaviour. Here is a simple sample to run - just uncomment out the method calls one at a time to see what happens.

----- code start -----

   using System;
  using System.Runtime.CompilerServices;
  using System.Threading;
  using System.Threading.Tasks;

  namespace AsyncAndExceptions
  {

    class Program
    {
      static void Main(string[] args)
      {
        AppDomain.CurrentDomain.UnhandledException += (s, e) => Log("*** Crash! ***", "UnhandledException");
        TaskScheduler.UnobservedTaskException += (s, e) => Log("*** Crash! ***", "UnobservedTaskException");

        RunTests();

        // Let async tasks complete...
        Thread.Sleep(500);
        GC.Collect(3, GCCollectionMode.Forced, true);
      }

      private static async Task RunTests()
      {
        try
        {
          // crash
          // _1_VoidNoWait();

          // crash 
          // _2_AsyncVoidAwait();

          // OK
          // _3_AsyncVoidAwaitWithTry();

          // crash - no await
          // _4_TaskNoWait();

          // crash - no await
          // _5_TaskAwait();

          // OK
          // await _4_TaskNoWait();

          // OK
          // await _5_TaskAwait();
        }
        catch (Exception ex) { Log("Exception handled OK"); }

        // crash - no try
        // await _4_TaskNoWait();

        // crash - no try
        // await _5_TaskAwait();
      }

      // Unsafe
      static void _1_VoidNoWait()
      {
        ThrowAsync();
      }

      // Unsafe
      static async void _2_AsyncVoidAwait()
      {
        await ThrowAsync();
      }

      // Safe
      static async void _3_AsyncVoidAwaitWithTry()
      {
        try { await ThrowAsync(); }
        catch (Exception ex) { Log("Exception handled OK"); }
      }

      // Safe only if caller uses await (or Result) inside a try
      static Task _4_TaskNoWait()
      {
        return ThrowAsync();
      }

      // Safe only if caller uses await (or Result) inside a try
      static async Task _5_TaskAwait()
      {
        await ThrowAsync();
      }

      // Helper that sets an exception asnychronously
      static Task ThrowAsync()
      {
        TaskCompletionSource tcs = new TaskCompletionSource();
        ThreadPool.QueueUserWorkItem(_ => tcs.SetException(new Exception("ThrowAsync")));
        return tcs.Task;
      }
      internal static void Log(string message, [CallerMemberName] string caller = "")
      {
        Console.WriteLine("{0}: {1}", caller, message);
      }
    }
  }

----- code end ----- 

From this we can see that the common advice isn't actually true:

  • It's perfectly fine to have an async void method (such as an event handler) so long as you look after exceptions within that method
  • Simply changing the signature doesn't prevent unhandled exceptions

The basic reason for this is that if you don't attempt to get the result of a Task (either by using await or by getting the Result directly) then it just sits there, holding on to the exception object, waiting to get GCed. During GC, it notices that nobody ever checked the result (and therefore never saw the exception) and so bubbles it up as an unobserved exception. As soon as someone asks for the result, the Task throws the exception instead which must then be caught by someone.

Comments

  • Anonymous
    December 10, 2014
    The comment has been removed
  • Anonymous
    January 31, 2018
    I have tried every possible permutation of await, async, GetAwait(), ConfigureAwait(false) possible. SPOCK is never ever caught.namespace ConsoleApp1{ class Program { static void Main(string[] args) { try { RunMe rm = new RunMe(); Task.Run(() => rm.run()).Wait(); } catch (Exception ex) { Console.WriteLine("Caught in Program"); } } } public class RunMe { public async Task run() { try { var h = new Hello(); List strings = new List(); strings.Add("1"); strings.Add("12"); await Task.Run(() => { strings.ForEach(async s => { await h.mike(); }); }); } catch (Exception ex) { Console.WriteLine("EXCEPTION:" + ex.Message); } } } public class Hello { public async Task mike() { throw new Exception("SPOCK"); } }}