1. Introducción
Imagínate: lanzas una decena de tareas asíncronas a la vez — parseas sitios, chequeas precios en marketplaces o procesas grandes arrays de datos. Todo arranca bien, pero varias tareas fallan con errores: en algún sitio el servidor no responde, en otro hay problema con los datos. En código síncrono verías una excepción, pero en asíncrono puede haber varias. Aquí entra en escena AggregateException, que agrupa todos los errores en un “contenedor”.
¿Qué es AggregateException?
AggregateException — un tipo especial de excepción en .NET que agrupa errores de operaciones paralelas/asíncronas: Task.WhenAll, Parallel.For, TPL, etc. Cuando dos o más tareas terminan con error, se “empaquetan” en un solo objeto AggregateException.
¿Dónde aparece AggregateException?
El escenario más común — esperar varias tareas con Task.WhenAll:
// Ejemplo — llamamos varias tareas en paralelo, algunas tiran excepción
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("Error #1")),
Task.Run(() => throw new ArgumentException("Error #2")),
Task.Run(() => { Console.WriteLine("¡La tarea 3 terminó con éxito!"); })
};
try
{
await Task.WhenAll(tasks); // Con await aquí AggregateException se despliega
}
catch (Exception ex)
{
Console.WriteLine($"Ocurrió un error: {ex.GetType().Name} - {ex.Message}");
}
Pero hay un matiz: con await AggregateException se despliega, y se lanza la primera excepción interna “tal cual”. Si no usas await y accedes a Result o llamas Wait(), obtendrás exactamente AggregateException.
2. ¿Cómo luce AggregateException?
Es una excepción con una colección InnerExceptions — con todas las fallas internas:
try
{
Task task = Task.WhenAll(tasks);
task.Wait(); // Aquí habrá exactamente AggregateException, si hubo errores
}
catch (AggregateException aggEx)
{
Console.WriteLine($"Total de errores: {aggEx.InnerExceptions.Count}");
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"Tipo: {e.GetType().Name}, Mensaje: {e.Message}");
}
}
Ejemplo de salida:
Total de errores: 2
Tipo: InvalidOperationException, Mensaje: Error #1
Tipo: ArgumentException, Mensaje: Error #2
Cómo surgen las AggregateException
+--------------------------+
| Lanzamos varias Task'os |
+-----+---------+----------+
| |
v v
Task1 Task2 ... TaskN
| |
| (falla con error)
|-------------------+
| v
| Exception2
v
Exception1
|
v
+---------------------------------+
| Task.WhenAll o Parallel.For |
| (agrega todas |
| las excepciones) |
+---------------------------------+
|
v
AggregateException
(InnerExceptions: todos los errores)
¿Por qué con await no se ve AggregateException?
Con await .NET “despliega” AggregateException y lanza la primera excepción interna (InnerExceptions[0]).
// Solo una tarea que lanza InvalidOperationException
try
{
await Task.Run(() => throw new InvalidOperationException("¡Error!"));
}
catch (Exception ex)
{
// Aquí ex — exactamente InvalidOperationException, no AggregateException
Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}
Si quieres acceder a todas las fallas — usa Wait() o Result (pero recuerda el bloqueo de hilo).
3. Capturar y manejar todas las excepciones
Vinculemos teoría con práctica. Supongamos que varias tareas procesan datos de usuarios y necesitas registrar todos los fallos.
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("The wrong operation!")),
Task.Run(() => throw new DivideByZeroException("Division by zero!")),
Task.Run(() => Console.WriteLine("La tercera tarea pasó con éxito"))
};
try
{
// Esperamos las tareas de forma asíncrona — obtenemos la primera excepción interna
Task allTasks = Task.WhenAll(tasks);
await allTasks; // con await AggregateException se despliega
}
catch (Exception ex)
{
// Con await veremos la primera excepción interna
Console.WriteLine($"Primer error: {ex.GetType().Name} - {ex.Message}");
// Si es AggregateException — podemos iterar por todas
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"Error de la lista: {inner.GetType().Name} - {inner.Message}");
}
}
}
// Ahora capturamos todos los errores de forma síncrona
try
{
var allTasks = Task.WhenAll(tasks);
allTasks.Wait(); // bloquea el hilo — ¡no hagas esto en UI!
}
catch (AggregateException aggEx)
{
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"[Wait] Error: {e.GetType().Name} — {e.Message}");
}
}
}
}
}
Con await en el bloque try/catch verás solo el primer error. Con Wait() llegará el clásico AggregateException con todas las excepciones internas.
4. Manejo de errores masivos
En apps reales a menudo necesitas tratar muchos errores de formas distintas. Por ejemplo, envías e-mails a todos los usuarios y parte de los envíos falla:
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("Error al enviar el correo: " + ex.Message);
else
Console.WriteLine("Error desconocido: " + ex.Message);
}
}
Así registras cada fallo y puedes tomar medidas individuales.
5. Matices útiles
Razones de este comportamiento
.NET parte de la conveniencia: en UI normalmente basta con la primera excepción (con await). Pero en servidores o sistemas donde importa manejar todos los fallos, AggregateException es indispensable.
Pattern: Flatten — desplegar la lista de errores
El método Flatten() convierte AggregateException anidados en una lista plana:
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.Flatten().InnerExceptions)
{
Console.WriteLine($"Error desplegado: {ex.GetType().Name} — {ex.Message}");
}
}
¿Qué comportamiento tiene Task con distintas formas de esperar?
| Cómo esperamos la tarea | Qué excepción atraparemos |
|---|---|
|
La primera excepción interna |
|
Siempre AggregateException |
|
Siempre AggregateException |
| Await individual | La excepción lanzada por la tarea, o la primera interna si la tarea terminó con AggregateException |
Importante recordar
- AggregateException es útil en todos los escenarios donde puede haber muchos errores simultáneos.
- Para await es cómodo capturar la primera excepción; para espera síncrona — recorrer todas las InnerExceptions.
- Usa Flatten() y Handle() para un manejo más fino.
- No mezcles esperas síncronas y asíncronas en UI: tendrás freezes y deadlocks.
7. Errores típicos al manejar AggregateException
Mito: await Task.WhenAll siempre lanza AggregateException. En realidad — no, la excepción se despliega y verás la primera interna.
Acceder a Result o llamar a Wait() bloquea el hilo hasta completar las tareas. En UI eso causará congelamiento de la interfaz.
Si quieres manejar parte de los errores “silenciosamente”, usa Handle():
catch (AggregateException aggEx)
{
aggEx.Handle(ex =>
{
if (ex is InvalidOperationException)
{
// Este error lo manejamos, no hace falta volver a lanzarlo
return true;
}
return false; // Las otras excepciones se volverán a lanzar
});
}
GO TO FULL VERSION