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 |
|---|---|
|
Primeira exceção interna |
|
Sempre AggregateException |
|
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
});
}
GO TO FULL VERSION