C# -  Programação Assíncrona - III


  Vamos continuar recordando conceitos importantes da programação assíncrona na linguagem C#

Continuando o artigo anterior vamos agora como tratar exceções na programação assícrona.

Tratamento de exceções

Na programação síncrona o código que lança uma exceção em um método síncrono se propaga para cima na pilha de chamadas até que a pilha de chamadas encontre um bloco catch que possa lidar com isso.

No entanto, gerenciar exceções em métodos assíncronos é mais complicado. Vejamos porquê :

No C#, um método assíncrono pode retornar um dos três valores: void, Task ou Task<T>.

As exceções lançadas em métodos assíncronos com tipos de retorno Task ou Task<T> são encapsuladas em uma instância AggregateException e associadas à instância Task.

Se mais de uma exceção for lançada, todas elas serão armazenadas no objeto Task.

Agora, os métodos assíncronos com um tipo de retorno void, por outro lado, não possuem um objeto Task associado a eles e assim, as exceções lançadas em um método async void não podem ser capturadas fora desse método.

Como exemplo deste comportamento podemos analisar o seguinte trecho de código:

TesteAsync t = new();
try

{
  t.ChamaTarefaAsync();
}

catch
(Exception ex)
{
  Console.WriteLine(ex.Message);
}

Console.ReadKey();

class TesteAsync
{
 
public Task MinhaTarefaAsync()
  {
   
return Task.Run(() => {
           Task.Delay(2000);
          
throw new Exception("Minha Exception...");
     });
  }
 
public async void ChamaTarefaAsync()
  {
    
await MinhaTarefaAsync();
  }
}

Ao executar o código acima o método ChamaTarefaAsync() será chamado e vai invocar o método MinhaTarefaAsync().

Neste método temos a execução de uma tarefa assíncrona que vai lançar uma exceção depois de 2 segundos : throw new Exception('Minha Exception...);

 Estamos esperando que o bloco catch trate a exceção mas não é isso que acontece.

O bloco catch não está tratando a exceção? Por que ?

O motivo é aqui o código executado é um código assíncrono por natureza, e, na programação assíncrona, o controle não espera pelo resultado do método e executa a próxima linha.

Portanto, quando o método lança uma exceção, nesse momento o controle do programa está fora do bloco try-catch. Este é o motivo da exceção não ser tratada pelo bloco catch.

Neste caso para capturar a exceção podemos alterar o código da seguinte forma:

TesteAsync t = new();
t.ChamaTarefaAsync();
Console.WriteLine(ex.Message);

Console.ReadKey();

class TesteAsync
{
 
public Task MinhaTarefaAsync()
  {
   
return Task.Run(() => {
           Task.Delay(2000);
          
throw new Exception("Minha Exception...");
     });
  }
 
public async void ChamaTarefaAsync()
  {
    
try
     {
       
await MinhaTarefaAsync();
     }
    
catch (Exception ex)
     {
       Console.WriteLine(ex.Message);
     }
   }
}

Agora a chamada do método ChamaTarefaAsync() na classe Main não precisa usar um bloco try-cath. O bloco try-catch pode ser usado dentro do método ChamaTarefaAsync() pois dentro do método temos uma execução síncrona e assim o bloco catch vai capturar a exceção:

Tratando exceções em métodos Assíncronos

Se uma exceção ocorrer dentro de um método assíncrono retornando um objeto Task, a exceção será agrupada em uma instância da classe AggregateException e passada de volta para o método de chamada.

O ponto importante a ser observado aqui é que, ao aguardar (await) a instância de Task, você poderá obter apenas a primeira exceção, mesmo que várias exceções possam ter sido lançadas em seu método assíncrono.

Para ilustrar este comportamento vemos o código a seguir:

await LancaMultiplasExcecoesAsync();
Console.ReadKey();
static async Task LancaMultiplasExcecoesAsync()
{
    try
    {
        var primeiraTask = Task.Run(() => {
            Task.Delay(1000);
            throw new IndexOutOfRangeException
            ("IndexOutOfRangeException lançada explicitamente.");
            });
        var segundaTask = Task.Run(() => {
            Task.Delay(1000);
            throw new InvalidOperationException
            ("InvalidOperationException lançada explicitamente");
        });
        await Task.WhenAll(primeiraTask, segundaTask);
    }
    catch (IndexOutOfRangeException ex)
    {
        Console.WriteLine(ex.Message);
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Executando o código teremos o seguinte resultado:

Observe que obtemos apenas uma exceção embora a execução das tarefas lançe duas exceções.

O que esta acontencendo aqui ?

Quando uma exceção é lançada em um método assíncrono que retorna um objeto Task, todas as exceções são colocadas na instância de Task e retornadas. Se houver várias exceções e você aguardar(await)  a  Task (Tarefa) como normalmente faz, receberá apenas uma exceção.

Ao executar o trecho de código acima, você observará que apenas a primeira mensagem da exceção é exibida. Em outras palavras, mesmo que duas exceções tenham sido lançadas no método assíncrono, você receberá apenas uma delas.

Em outras palavras, a palavra-chave await vai desempacotar apenas a primeira exceção da instância Task e desta froma  você vai obter apenas uma exceção.

Como podemos obter todas as exceções ?

Para obter todas as exceções lançadas podemos usar a coleção InnerExceptions da classe AggregateException que representa um ou mais erros que ocorrem durante a execução do aplicativo.

Vamos alterar o código conforme abaixo:

await LancaMultiplasExcecoesAsync();
Console.ReadKey();
static async Task LancaMultiplasExcecoesAsync()
{
    Task? tarefas = null;
    try
    {
        var primeiraTask = Task.Run(() => {
            Task.Delay(1000);
            throw new IndexOutOfRangeException
            ("IndexOutOfRangeException lançada explicitamente.");
            });
        var segundaTask = Task.Run(() => {
            Task.Delay(1000);
            throw new InvalidOperationException
            ("InvalidOperationException lançada explicitamente");
        });
        tarefas =  Task.WhenAll(primeiraTask, segundaTask);
        await tarefas;
    }
    catch
    {
        Console.WriteLine("Ocorreram as seguintes exceções :-\n");
        AggregateException TodasExceptions = tarefas.Exception;
        foreach (var ex in TodasExceptions.InnerExceptions)
        {
            Console.WriteLine(ex.GetType().ToString());
        }
    }
}

Executando o código teremos o seguinte resultado:

A  classe AggregateException é usada para representar uma ou mais exceções que ocorreram durante a execução de uma operação assíncrona. Quando várias tarefas assíncronas são executadas em paralelo e uma ou mais delas geram exceções, a AggregateException é usada para armazenar todas essas exceções em uma única exceção.

Desta forma a propriedade Exception da instância Task retorna uma referência AggregateException que contém todas as exceções lançadas em seu método assíncrono.

A classe AggregateException herda da classe Exception e contém uma propriedade chamada InnerExceptions, que é uma coleção de exceções que foram lançadas durante a execução assíncrona. Você pode iterar através das exceções individuais usando a propriedade InnerExceptions.

Assim no código acima definimos um objeto tarefas do tipo Task que no bloco catch definimos o código para obter todas as exceções lançadas e usando um laço foreach exibimos cada exceção :

        Console.WriteLine("Ocorreram as seguintes exceções :-\n");
        AggregateException TodasExceptions = tarefas.Exception;
        foreach (var ex in TodasExceptions.InnerExceptions)
        {
            Console.WriteLine(ex.GetType().ToString());
        }

É importante lembrar que, ao lidar com uma AggregateException, você deve examinar todas as exceções dentro da propriedade InnerExceptions, pois pode haver mais de uma exceção. Além disso, você deve tomar cuidado para não tratar exceções de forma inadequada, pois isso pode levar a um comportamento instável ou inesperado do seu aplicativo.

O método AggregateException.Handle permite manipular certas exceções enquanto ignora aquelas que você não deseja manipular.

No código a seguir temos um exemplo de como usar o método Handle:

await ExecutaTarefaAsync();
Console.ReadKey();
async static Task ExecutaTarefaAsync()
{
    try
    {
        var task1 = Task.Run(() => { 
                Task.Delay(1000);
        throw new IndexOutOfRangeException
            ("Lançando um IndexOutOfRangeException");
       });
       var task2 = Task.Run(() => {
           Task.Delay(1000);
           throw new ArithmeticException
           ("Lançando uma ArithmeticException.");
       });
        await Task.WhenAll(task1, task2);
    }
    catch (AggregateException ae)
    {
        ae.Handle(ex =>
        {
            if (ex is IndexOutOfRangeException)
            {
                Console.WriteLine(ex.Message);
            }
            return ex is IndexOutOfRangeException;
        });
    }
}

Neste código criamos duas tarefas assíncronas usando o método Task.Run(), cada uma delas com um atraso de 1 segundo e lançando uma exceção específica. Em seguida, as duas tarefas são executadas em paralelo usando o método Task.WhenAll(). O código usa a palavra-chave await para aguardar o término das tarefas antes de continuar a execução.

Se alguma das tarefas gerar uma exceção, o bloco catch  vai capturar a exceção e usará o método Handle() da classe AggregateException para lidar com a exceção. Este método  permite que você processe todas as exceções na coleção InnerExceptions e determine quais exceções devem ser tratadas e quais devem ser ignoradas.

No exemplo acima usamos o método Handle() para verificar se a exceção é uma IndexOutOfRangeException. Se for, a mensagem de erro da exceção é impressa no console.

A seguir o método Handle() retorna um valor booleano indicando se a exceção foi tratada com sucesso. Se o valor de retorno for true, a exceção será considerada tratada e não será propagada para o código cliente.

Podemos aproveitar a programação assíncrona para criar aplicativos escaláveis e responsivos. No entanto, ao usar métodos assíncronos, lembre-se de que a semântica de tratamento de erros desses métodos é diferente daquela usada nos métodos síncronos.

E estamos conversados...

Referências:


José Carlos Macoratti