Capturar uma exceção lançada por um método de async void


282

Usando o CTP assíncrono da Microsoft para .NET, é possível capturar uma exceção lançada por um método assíncrono no método de chamada?

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

Então, basicamente, quero que a exceção do código assíncrono entre no meu código de chamada, se isso for possível.



22
Caso alguém se depare com isso no futuro, o artigo Async / Await Best Practices ... tem uma boa explicação sobre isso na "Figura 2 Exceções de um método de vácuo assíncrono não podem ser capturadas". " Quando uma exceção é lançada fora de um método Task assíncrono ou Tarefa assíncrona <T>, essa exceção é capturada e colocada no objeto Task. Nos métodos assíncronos nulos, não há objeto Tarefa, nenhuma exceção lançada fora de um método assíncrono vazio será gerado diretamente no SynchronizationContext que estava ativo quando o método assíncrono vazio foi iniciado. "
Mr Moose

Você pode usar essa abordagem ou esta
Tselofan

Respostas:


262

É um tanto estranho ler, mas sim, a exceção ocorrerá no código de chamada - mas somente se você awaitou Wait()a chamadaFoo .

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

Os métodos de vácuo assíncrono têm diferentes semânticas de tratamento de erros. Quando uma exceção é lançada de um método de tarefa assíncrona ou tarefa assíncrona, essa exceção é capturada e colocada no objeto Tarefa. Com os métodos async void, não há objeto Task, portanto, as exceções lançadas fora de um método async void serão geradas diretamente no SynchronizationContext que estava ativo quando o método async void foi iniciado. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Observe que o uso de Wait () pode bloquear o seu aplicativo, se o .Net decidir executar seu método de forma síncrona.

Essa explicação http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions é muito boa - discute as etapas que o compilador executa para alcançar essa mágica.


3
Eu realmente significa que é simples e direta para ler - ao passo que eu sei o que está realmente acontecendo é realmente complicado - assim que meu cérebro está me dizendo para não acreditar nos meus olhos ...
Stuart

8
Eu acho que o método Foo () deve ser marcado como tarefa em vez de nulo.
Sornii 26/03

4
Tenho certeza de que isso produzirá uma AggregateException. Como tal, o bloco catch, como aparece nesta resposta, não capturará a exceção.
Xanadont 9/05

2
"mas apenas se você aguardar ou aguardar () a ligação para Foo" Como você pode awaitligar para Foo quando Foo está retornando nulo? async void Foo(). Type void is not awaitable?
RISM

3
Não pode esperar o método nulo, pode?
Hitesh P

74

A razão pela qual a exceção não foi detectada é porque o método Foo () tem um tipo de retorno nulo e, portanto, quando aguardar é chamado, ele simplesmente retorna. Como o DoFoo () não está aguardando a conclusão do Foo, o manipulador de exceções não pode ser usado.

Isso abre uma solução mais simples se você pode alterar as assinaturas do método - altere Foo()para que ele retorne o tipo Taske depois DoFoo()possa await Foo(), como neste 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
    }
}

19
Isso pode realmente te surpreender e deve ser avisado pelo compilador.
GGleGrand 18/03/19

19

Seu código não faz o que você pensa que faz. Os métodos assíncronos retornam imediatamente após o método começar a aguardar o resultado assíncrono. É interessante usar o rastreamento para investigar como o código está realmente se comportando.

O código abaixo faz o seguinte:

  • Crie 4 tarefas
  • Cada tarefa incrementará assincronamente um número e retornará o número incrementado
  • Quando o resultado assíncrono chega, ele é rastreado.

 

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

Quando você observa os traços

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   

Você notará que o método Run é concluído no thread 2820 enquanto apenas um thread filho foi concluído (2756). Se você colocar um try / catch em torno de seu método wait, poderá "capturar" a exceção da maneira usual, embora seu código seja executado em outro encadeamento quando a tarefa de cálculo for concluída e sua execução for executada.

O método de cálculo rastreia a exceção lançada automaticamente porque eu usei o ApiChange.Api.dll da ferramenta ApiChange . Rastreamento e Refletor ajuda muito a entender o que está acontecendo. Para se livrar do encadeamento, você pode criar suas próprias versões do GetAwaiter BeginAwait e EndAwait e agrupar não uma tarefa, mas, por exemplo, um Lazy e um rastreamento dentro de seus próprios métodos de extensão. Então você entenderá muito melhor o que o compilador e o que o TPL faz.

Agora você vê que não há como tentar / recuperar sua exceção, pois não há um quadro de pilha restante para que nenhuma exceção se propague. Seu código pode estar fazendo algo totalmente diferente depois que você iniciou as operações assíncronas. Pode chamar Thread.Sleep ou até terminar. Enquanto houver um encadeamento em primeiro plano, seu aplicativo continuará felizmente executando tarefas assíncronas.


Você pode manipular a exceção dentro do método assíncrono após a conclusão da operação assíncrona e chamar novamente o thread da interface do usuário. A maneira recomendada de fazer isso é com TaskScheduler.FromSynchronizationContext . Isso funciona apenas se você tiver um thread da interface do usuário e não estiver muito ocupado com outras coisas.


5

A exceção pode ser capturada na função assí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();
}

2
Ei, eu sei, mas eu realmente preciso dessas informações no DoFoo para que eu possa exibir as informações na interface do usuário. Neste caso, é importante para a interface do usuário para exibir a exceção, pois não é uma ferramenta de usuário final, mas uma ferramenta para depurar um protocolo de comunicação
TimothyP

Nesse caso, as chamadas de retorno fazer um monte de sentido (bons velhos delegados assíncronos).
Sanjeevakumar Hiremath

@ Tim: Inclua as informações necessárias na exceção lançada?
Eric J.

5

Também é importante observar que você perderá o rastreamento da pilha cronológica da exceção se tiver um tipo de retorno nulo em um método assíncrono. Eu recomendaria retornar a tarefa da seguinte maneira. Vai facilitar muito a depuração.

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

Isso causará um problema com nem todos os caminhos retornando um valor, pois se houver uma exceção, nenhum valor será retornado, enquanto na tentativa houver. Se você não tiver nenhuma returninstrução, esse código funcionará, pois Taské retornado "implicitamente" usando async / await.
Matias Grioni 22/02/19

2

Este blog explica claramente o seu problema Async Best Practices .

A essência disso é que você não deve usar o void como retorno para um método assíncrono, a menos que seja um manipulador de eventos assíncronos, isso é uma prática ruim porque não permite que exceções sejam capturadas ;-).

A melhor prática seria alterar o tipo de retorno para Tarefa. Além disso, tente codificar async completamente, faça todas as chamadas de método assíncrono e seja chamado a partir de métodos assíncronos. Exceto pelo método Main em um console, que não pode ser assíncrono (antes do C # 7.1).

Você encontrará conflitos com aplicativos GUI e ASP.NET se ignorar esta prática recomendada. O impasse ocorre porque esses aplicativos são executados em um contexto que permite apenas um encadeamento e não o abandona ao encadeamento assíncrono. Isso significa que a GUI espera síncrona por um retorno, enquanto o método assíncrono aguarda o contexto: deadlock.

Esse comportamento não acontece em um aplicativo de console, porque é executado no contexto com um pool de threads. O método assíncrono retornará em outro thread que será agendado. É por isso que um aplicativo de console de teste funcionará, mas as mesmas chamadas entrarão em conflito em outros aplicativos ...


1
"Exceto pelo método Main em um console, que não pode ser assíncrono." Desde C # 7.1, Main agora pode ser um método assíncrono ligação
Adam
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.