Captura una excepción lanzada por un método asíncrono

Usando el async CTP de Microsoft para .NET, ¿es posible detectar una excepción lanzada por un método asíncrono en el método de llamada?

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. */ } } 

Así que, básicamente, quiero que la excepción del código asincrónico brote en mi código de llamada si eso es posible.

Es algo raro de leer, pero sí, la excepción saltará hasta el código de llamada, pero solo si await o Wait() la llamada a Foo .

 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. */ } } 

Los métodos Async void tienen una semántica distinta para el manejo de errores. Cuando se descarta una excepción de una tarea asíncrona o un método de tarea asincrónica, esa excepción se captura y se coloca en el objeto Tarea. Con los métodos de anync void, no hay ningún objeto Task, por lo que cualquier excepción lanzada fuera de un método async void se generará directamente en el SynchronizationContext que estaba activo cuando se inició el método async void. – https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Tenga en cuenta que usar Wait () puede hacer que su aplicación se bloquee, si .Net decide ejecutar su método de forma síncrona.

Esta explicación http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions es bastante buena: discute los pasos que el comstackdor realiza para lograr esta magia.

El motivo por el que no se detecta la excepción es porque el método Foo () tiene un tipo de retorno nulo y, por lo tanto, cuando se invoca, se devuelve. Como DoFoo () no está esperando la finalización de Foo, el manejador de excepciones no se puede usar.

Esto abre una solución más simple si puede cambiar las firmas de métodos – alterar Foo() para que retorne el tipo Task y luego DoFoo() pueda await Foo() , como en este código:

 public async Task Foo() { var x = await DoSomethingThatThrows(); } public async void DoFoo() { try { await Foo(); } catch (ProtocolException ex) { // This will catch exceptions from DoSomethingThatThrows } } 

Tu código no hace lo que crees que hace. Los métodos asíncronos regresan inmediatamente después de que el método comienza a esperar el resultado de la sincronización. Es útil utilizar el seguimiento para investigar cómo se comporta realmente el código.

El siguiente código hace lo siguiente:

  • Crea 4 tareas
  • Cada tarea incrementará de forma asincrónica un número y devolverá el número incrementado
  • Cuando el resultado de la sincronización ha llegado, se rastrea.
 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 Calculate(int i) { var t = new Task(() => { 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; } 

Cuando observas las huellas

 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 

Notará que el método Run se completa en el subproceso 2820 mientras que solo un subproceso secundario ha finalizado (2756). Si pone try / catch alrededor de su método de espera, puede “atrapar” la excepción de la manera habitual, aunque su código se ejecuta en otro hilo cuando la tarea de cálculo ha finalizado y se ejecuta su contiuation.

El método de cálculo rastrea la excepción lanzada automáticamente porque utilicé ApiChange.Api.dll desde la herramienta ApiChange . El rastreo y el reflector ayudan mucho a comprender lo que está sucediendo. Para deshacerse del enhebrado puedes crear tus propias versiones de GetAwaiter BeginAwait y EndAwait y no envolver una tarea, por ejemplo, un Lazy y rastrear dentro de tus propios métodos de extensión. Entonces comprenderá mucho mejor qué hace el comstackdor y qué hace el TPL.

Ahora puede ver que no hay forma de intentar / recuperar su excepción ya que no existe un marco de stack para propagar desde ninguna excepción. Su código podría estar haciendo algo totalmente diferente después de iniciar las operaciones de sincronización. Podría llamar a Thread.Sleep o incluso terminar. Siempre que haya un subproceso en primer plano, su aplicación continuará felizmente ejecutando tareas asíncronas.


Puede manejar la excepción dentro del método async después de que su operación asincrónica haya finalizado y vuelva a llamar al hilo de la interfaz de usuario. La forma recomendada de hacerlo es con TaskScheduler.FromSynchronizationContext . Eso solo funciona si tienes un hilo UI y no está muy ocupado con otras cosas.

También es importante tener en cuenta que perderá el rastro de la stack cronológica de la excepción si tiene un tipo de retorno nulo en un método asíncrono. Recomendaría devolver Tarea de la siguiente manera. Ir a hacer la depuración mucho más fácil.

 public async Task DoFoo() { try { return await Foo(); } catch (ProtocolException ex) { /* Exception with chronological stack trace */ } } 

La excepción se puede atrapar en la función asíncrona.

 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(); } 

Este blog explica claramente tu problema Async Best Practices .

La esencia de esto es que no deberías usar void como devolución para un método asíncrono, a menos que sea un controlador de eventos asíncrono, esta es una mala práctica porque no permite capturar excepciones ;-).

La mejor práctica sería cambiar el tipo de devolución a Tarea. Además, intente codificar de manera asíncrona todo el canal, haga que cada método asíncrono llame y sea llamado desde métodos asíncronos. Excepto por un método principal en una consola, que no puede ser asincrónico (antes de C # 7.1).

Se encontrará con deadlocks con aplicaciones GUI y ASP.NET si ignora esta mejor práctica. El punto muerto ocurre porque estas aplicaciones se ejecutan en un contexto que permite solo un hilo y no lo abandonará al hilo asincrónico. Esto significa que la GUI espera sincrónicamente una devolución, mientras que el método async espera el contexto: punto muerto.

Este comportamiento no ocurrirá en una aplicación de consola, ya que se ejecuta en contexto con un grupo de subprocesos. El método asíncrono volverá en otro hilo que se progtwigrá. Esta es la razón por la que una aplicación de consola de prueba funcionará, pero las mismas llamadas se estancarán en otras aplicaciones …