Attraper une exception levée par une méthode async void

283

À l'aide du CTP asynchrone de Microsoft pour .NET, est-il possible d'attraper une exception levée par une méthode async dans la méthode appelante?

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

Donc, fondamentalement, je veux que l'exception du code asynchrone se propage dans mon code d'appel si c'est même possible.

TimothyP
la source
22
Au cas où quelqu'un trébucherait à ce sujet à l'avenir, l'article Async / Await Best Practices ... a une bonne explication à ce sujet dans "Figure 2 Les exceptions à une méthode Async Void ne peuvent pas être prises avec Catch". " Lorsqu'une exception est levée d'une méthode async Task ou async Task <T>, cette exception est capturée et placée sur l'objet Task. Avec les méthodes async void, il n'y a pas d'objet Task, aucune exception n'est levée d'une méthode async void sera levé directement sur le SynchronizationContext qui était actif lorsque la méthode async void a démarré. "
Mr Moose
Vous pouvez utiliser cette approche ou ceci
Tselofan

Réponses:

263

C'est un peu bizarre à lire, mais oui, l'exception va remonter jusqu'au code d'appel - mais seulement si vous awaitou Wait()l'appel à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. */
    }
} 

Les méthodes async void ont différentes sémantiques de gestion des erreurs. Lorsqu'une exception est levée d'une tâche asynchrone ou d'une méthode de tâche asynchrone, cette exception est capturée et placée sur l'objet Task. Avec les méthodes async void, il n'y a pas d'objet Task, donc toutes les exceptions levées d'une méthode async void seront levées directement sur le SynchronizationContext qui était actif lorsque la méthode async void a démarré. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Notez que l'utilisation de Wait () peut entraîner le blocage de votre application si .Net décide d'exécuter votre méthode de manière synchrone.

Cette explication http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions est assez bonne - elle décrit les étapes que le compilateur prend pour réaliser cette magie.

Stuart
la source
3
Je veux dire que c'est simple à lire - alors que je sais ce qui se passe réellement est vraiment compliqué - donc mon cerveau me dit de ne pas en croire mes yeux ...
Stuart
8
Je pense que la méthode Foo () devrait être marquée comme tâche au lieu de void.
Sornii
4
Je suis sûr que cela produira une exception AggregateException. En tant que tel, le bloc catch tel qu'il apparaît dans cette réponse n'attrapera pas l'exception.
xanadont
2
"mais seulement si vous attendez ou attendez () l'appel à Foo" Comment pouvez-vous awaitl'appel à Foo, quand Foo est de retour nul? async void Foo(). Type void is not awaitable?
rism
3
Ne peut pas attendre la méthode void, n'est-ce pas?
Hitesh P
74

La raison pour laquelle l'exception n'est pas interceptée est que la méthode Foo () a un type de retour void et donc lorsque l'attente est appelée, elle revient simplement. Comme DoFoo () n'attend pas la fin de Foo, le gestionnaire d'exceptions ne peut pas être utilisé.

Cela ouvre une solution plus simple si vous pouvez modifier les signatures de méthode - modifiez de Foo()sorte qu'il retourne le type Tasket puis DoFoo()pouvez await Foo(), comme dans ce 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
    }
}
Rob Church
la source
19
Cela peut vraiment vous surprendre et devrait être averti par le compilateur.
GGleGrand
19

Votre code ne fait pas ce que vous pourriez penser. Les méthodes asynchrones reviennent immédiatement après que la méthode commence à attendre le résultat asynchrone. Il est judicieux d'utiliser le traçage afin d'étudier le comportement réel du code.

Le code ci-dessous fait ce qui suit:

  • Créez 4 tâches
  • Chaque tâche incrémentera de manière asynchrone un nombre et renverra le nombre incrémenté
  • Lorsque le résultat asynchrone est arrivé, il est tracé.

 

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

Quand on observe les 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   

Vous remarquerez que la méthode Run se termine sur le thread 2820 alors qu'un seul thread enfant est terminé (2756). Si vous mettez un try / catch autour de votre méthode d'attente, vous pouvez "intercepter" l'exception de la manière habituelle bien que votre code soit exécuté sur un autre thread lorsque la tâche de calcul est terminée et que votre contiuation est exécutée.

La méthode de calcul trace automatiquement l'exception levée car j'ai utilisé ApiChange.Api.dll à partir de l' outil ApiChange . Le traçage et le réflecteur aident beaucoup à comprendre ce qui se passe. Pour vous débarrasser du filetage, vous pouvez créer vos propres versions de GetAwaiter BeginAwait et EndAwait et encapsuler non pas une tâche mais par exemple un paresseux et tracer à l'intérieur de vos propres méthodes d'extension. Ensuite, vous comprendrez mieux ce que le compilateur et ce que fait le TPL.

Vous voyez maintenant qu'il n'y a aucun moyen de récupérer / essayer votre exception, car il n'y a plus de cadre de pile pour la propagation d'une exception. Votre code peut faire quelque chose de totalement différent après avoir lancé les opérations asynchrones. Il peut appeler Thread.Sleep ou même se terminer. Tant qu'il reste un thread de premier plan, votre application continuera volontiers à exécuter des tâches asynchrones.


Vous pouvez gérer l'exception à l'intérieur de la méthode async une fois votre opération asynchrone terminée et rappeler dans le thread d'interface utilisateur. La méthode recommandée pour ce faire est avec TaskScheduler.FromSynchronizationContext . Cela ne fonctionne que si vous avez un thread d'interface utilisateur et qu'il n'est pas très occupé avec d'autres choses.

Alois Kraus
la source
5

L'exception peut être interceptée dans la fonction asynchrone.

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();
}
Sanjeevakumar Hiremath
la source
2
Hé, je sais mais j'ai vraiment besoin de ces informations dans DoFoo pour pouvoir afficher les informations dans l'interface utilisateur. Dans ce cas, il est important que l'interface utilisateur affiche l'exception car ce n'est pas un outil d'utilisateur final mais un outil pour déboguer un protocole de communication
TimothyP
Dans ce cas, les rappels ont beaucoup de sens. (Bons vieux délégués asynchrones)
Sanjeevakumar Hiremath
@Tim: inclure les informations dont vous avez besoin dans l'exception levée?
Eric J.21
5

Il est également important de noter que vous perdrez la trace chronologique de la pile de l'exception si vous avez un type de retour vide sur une méthode asynchrone. Je recommanderais de retourner la tâche comme suit. Va rendre le débogage beaucoup plus facile.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
rohanjansen
la source
Cela entraînera un problème avec tous les chemins ne retournant pas de valeur, car s'il y a une exception, aucune valeur n'est retournée, alors que dans l'essai, il y en a. Si vous n'avez aucune returninstruction, ce code fonctionne cependant, puisque le Taskest "implicitement" renvoyé en utilisant async / await.
Matias Grioni
2

Ce blog explique clairement votre problème Async Best Practices .

L'essentiel étant que vous ne devriez pas utiliser void comme retour pour une méthode async, sauf s'il s'agit d'un gestionnaire d'événement async, c'est une mauvaise pratique car elle ne permet pas de détecter les exceptions ;-).

La meilleure pratique serait de changer le type de retour en tâche. Essayez également de coder async à fond, effectuez chaque appel de méthode async et soyez appelé à partir des méthodes async. Sauf pour une méthode Main dans une console, qui ne peut pas être asynchrone (avant C # 7.1).

Vous rencontrerez des blocages avec les applications GUI et ASP.NET si vous ignorez cette meilleure pratique. Le blocage se produit car ces applications s'exécutent dans un contexte qui n'autorise qu'un seul thread et ne le renoncent pas au thread asynchrone. Cela signifie que l'interface graphique attend de manière synchrone un retour, tandis que la méthode async attend le contexte: blocage.

Ce comportement ne se produira pas dans une application console, car il s'exécute sur le contexte avec un pool de threads. La méthode async reviendra sur un autre thread qui sera planifié. C'est pourquoi une application de console de test fonctionnera, mais les mêmes appels se bloqueront dans d'autres applications ...

Stephan Ghequiere
la source
1
"Sauf pour une méthode Main dans une console, qui ne peut pas être asynchrone." Depuis C # 7.1, Main peut désormais être un lien de
Adam