CodeGym /Cursos /C# SELF /Várias exceções: Aggregate...

Várias exceções: AggregateException

C# SELF
Nível 61, Lição 1
Disponível

1. Introdução

Imagine: você dispara de uma vez umas dez tarefas assíncronas — faz parsing de sites, checa preços em marketplaces ou processa grandes arrays de dados. Tudo começa bem, mas algumas tarefas falham: em algum lugar o servidor não responde, em outro há problema nos dados. No código síncrono você veria uma exceção só, mas no assíncrono podem vir várias. É aí que entra o AggregateException, que reúne todos os erros em um “container”.

O que é AggregateException?

AggregateException é um tipo especial de exceção no .NET que agrega erros vindos de operações paralelas/assíncronas: Task.WhenAll, Parallel.For, TPL e afins. Quando duas ou mais tasks terminam com erro, elas são “embaladas” em um único objeto AggregateException.

Onde surge AggregateException?

O cenário mais comum é esperar várias tasks com Task.WhenAll:

// Exemplo — chamamos várias tasks em paralelo, algumas delas lançam exceção
var tasks = new List<Task>
{
    Task.Run(() => throw new InvalidOperationException("Erro #1")),
    Task.Run(() => throw new ArgumentException("Erro #2")),
    Task.Run(() => { Console.WriteLine("Tarefa 3 terminou com sucesso!"); })
};

try
{
    await Task.WhenAll(tasks); // Com await aqui o AggregateException é "desembrulhado"
}
catch (Exception ex)
{
    Console.WriteLine($"Ocorreu um erro: {ex.GetType().Name} - {ex.Message}");
}

Mas tem uma nuance: com await o AggregateException é desembrulhado e a primeira exceção interna é lançada "como está". Se você não usar await, e acessar Result ou chamar Wait(), você receberá o próprio AggregateException.

2. Como é um AggregateException?

Essa exceção tem uma coleção InnerExceptions — todas as exceções aninhadas:

try
{
    Task task = Task.WhenAll(tasks);
    task.Wait(); // Aqui será exatamente AggregateException, se houveram erros
}
catch (AggregateException aggEx)
{
    Console.WriteLine($"Total de erros: {aggEx.InnerExceptions.Count}");
    foreach (var e in aggEx.InnerExceptions)
    {
        Console.WriteLine($"Tipo: {e.GetType().Name}, Mensagem: {e.Message}");
    }
}

Exemplo de saída:

Total de erros: 2
Tipo: InvalidOperationException, Mensagem: Erro #1
Tipo: ArgumentException, Mensagem: Erro #2

Como surgem AggregateException


+--------------------------+
| Disparamos várias        |
|   Tasks                  |
+-----+---------+----------+
      |         |
      v         v
  Task1    Task2 ... TaskN
   |          |
   |  (falha com erro)
   |-------------------+
   |                   v
   |              Exception2
   v
Exception1

   |
   v
+---------------------------------+
| Task.WhenAll ou Parallel.For    |
|       (agrega todas as          |
|        exceções)                |
+---------------------------------+
              |
              v
     AggregateException
   (InnerExceptions: todas as falhas)

Por que com await não vemos o AggregateException?

Com await o .NET “desembrulha” o AggregateException e lança a primeira exceção interna (InnerExceptions[0]).

// Só uma tarefa que lança InvalidOperationException
try
{
    await Task.Run(() => throw new InvalidOperationException("Erro!"));
}
catch (Exception ex)
{
    // Aqui ex é exatamente InvalidOperationException, não AggregateException
    Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}

Se quiser acessar todas as erros, use Wait() ou Result (mas lembre-se do bloqueio de thread).

3. Capturando e tratando todas as exceções

Vamos ligar teoria com prática. Suponha que várias tarefas processam dados do usuário, e você precisa registrar todas as falhas.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var tasks = new List<Task>
            {
                Task.Run(() => throw new InvalidOperationException("Operação inválida!")),
                Task.Run(() => throw new DivideByZeroException("Divisão por zero!")),
                Task.Run(() => Console.WriteLine("Terceira tarefa completou com sucesso"))
            };

            try
            {
                // Esperamos as tasks assincronamente — recebemos a primeira exceção interna
                Task allTasks = Task.WhenAll(tasks);
                await allTasks; // com await AggregateException é desembrulhado
            }
            catch (Exception ex)
            {
                // Com await veremos a primeira exceção interna
                Console.WriteLine($"Primeiro erro: {ex.GetType().Name} - {ex.Message}");

                // Se for AggregateException — podemos iterar por todas
                if (ex is AggregateException aggEx)
                {
                    foreach (var inner in aggEx.InnerExceptions)
                    {
                        Console.WriteLine($"Erro da lista: {inner.GetType().Name} - {inner.Message}");
                    }
                }
            }

            // Agora capturamos todos os erros de forma síncrona
            try
            {
                var allTasks = Task.WhenAll(tasks);
                allTasks.Wait(); // bloqueia a thread — não faça isso na UI!
            }
            catch (AggregateException aggEx)
            {
                foreach (var e in aggEx.InnerExceptions)
                {
                    Console.WriteLine($"[Wait] Erro: {e.GetType().Name} — {e.Message}");
                }
            }
        }
    }
}

Com await no bloco try/catch você verá só o primeiro erro. Com Wait() vem o clássico AggregateException com todas as exceções internas.

4. Tratamento de erros em massa

Em apps reais muitas vezes você precisa tratar vários erros de formas diferentes. Por exemplo, você envia e-mails para todos os usuários e algumas remessas falham:

var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();

try
{
    Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
    {
        if (ex is SmtpException)
            Console.WriteLine("Erro no envio do e-mail: " + ex.Message);
        else
            Console.WriteLine("Erro desconhecido: " + ex.Message);
    }
}

Assim você registra cada falha e pode tomar ações individuais.

5. Dicas úteis

Por que esse comportamento acontece

O .NET privilegia a conveniência: em UI geralmente basta a primeira exceção (com await). Mas em servidores ou sistemas onde é importante tratar todas as falhas, o AggregateException é indispensável.

Padrão: Flatten — achatar a lista de erros

O método Flatten() transforma AggregateExceptions aninhados em uma lista plana:

catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.Flatten().InnerExceptions)
    {
        Console.WriteLine($"Erro achatado: {ex.GetType().Name} — {ex.Message}");
    }
}

Como o Task se comporta dependendo da espera

Como esperamos a task Que exceção vamos pegar
await Task.WhenAll()
Primeira exceção interna
Task.WhenAll().Wait()
Sempre AggregateException
Task.WhenAll().Result
Sempre AggregateException
Await individual Exceção lançada pela tarefa, ou primeira interna se a task resultou em AggregateException

Importante lembrar

  • AggregateException é útil em cenários onde muitos erros podem ocorrer ao mesmo tempo.
  • Com await é conveniente capturar a primeira falha; com espera síncrona — iterar por todas as InnerExceptions.
  • Use Flatten() e Handle() para um tratamento mais controlado.
  • Não misture espera síncrona e assíncrona na UI: você vai ter freezes e deadlocks.

7. Erros comuns ao lidar com AggregateException

Mito: await Task.WhenAll sempre lança AggregateException. Na verdade — não: a exceção é desembrulhada e você verá a primeira interna.

Acessar Result ou chamar Wait() bloqueia a thread até as tasks terminarem. Na UI isso causará travamentos.

Se quiser tratar parte dos erros "silenciosamente", use Handle():

catch (AggregateException aggEx)
{
    aggEx.Handle(ex =>
    {
        if (ex is InvalidOperationException)
        {
            // Esse erro a gente tratou, não precisa relançar
            return true;
        }
        return false; // As demais exceções serão relançadas
    });
}
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION