1. Introduzione
Immagina: lanci decine di task asincroni in parallelo — parsifichi siti, controlli prezzi sui marketplace o esegui calcoli su grandi array. Tutto parte bene, ma alcuni task falliscono con errori: in un caso il server non risponde, in un altro ci sono dati sbagliati. Nel codice sincrono vedresti una sola eccezione, mentre in asincrono possono esserci più errori. Qui entra in gioco AggregateException, che raccoglie tutti gli errori in un unico "contenitore".
Cos'è AggregateException?
AggregateException è un tipo speciale di eccezione in .NET che aggrega gli errori provenienti da operazioni parallele/asincrone: Task.WhenAll, Parallel.For, TPL ecc. Quando due o più task terminano con errore, vengono "impacchettati" in un singolo oggetto AggregateException.
Dove si verifica AggregateException?
Lo scenario più comune è l'attesa di più task tramite Task.WhenAll:
// Esempio — eseguiamo più task in parallelo, alcuni dei quali lanciano eccezioni
var tasks = new List<Task>
{
Task.Run(() => throw new InvalidOperationException("Errore #1")),
Task.Run(() => throw new ArgumentException("Errore #2")),
Task.Run(() => { Console.WriteLine("Task 3 è stato completato con successo!"); })
};
try
{
await Task.WhenAll(tasks); // Con await qui AggregateException viene "svolto"
}
catch (Exception ex)
{
Console.WriteLine($"Si è verificato un errore: {ex.GetType().Name} - {ex.Message}");
}
Ma c'è un dettaglio: con await il AggregateException viene svolto e viene rilanciata la prima eccezione interna "così com'è". Se invece non usi await e accedi a Result o chiami Wait(), otterrai proprio un AggregateException.
2. Com'è fatto AggregateException?
Questa eccezione contiene una collezione InnerExceptions — tutte le eccezioni annidate:
try
{
Task task = Task.WhenAll(tasks);
task.Wait(); // Qui si otterrà proprio AggregateException, se ci sono stati errori
}
catch (AggregateException aggEx)
{
Console.WriteLine($"Totale errori: {aggEx.InnerExceptions.Count}");
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"Tipo: {e.GetType().Name}, Messaggio: {e.Message}");
}
}
Esempio di output:
Totale errori: 2
Tipo: InvalidOperationException, Messaggio: Errore #1
Tipo: ArgumentException, Messaggio: Errore #2
Come si generano le AggregateException
+--------------------------+
| Avviamo diversi Task |
+-----+---------+----------+
| |
v v
Task1 Task2 ... TaskN
| |
| (cade con errore)
|-------------------+
| v
| Exception2
v
Exception1
|
v
+---------------------------------+
| Task.WhenAll o Parallel.For |
| (raccoglie tutte |
| le eccezioni) |
+---------------------------------+
|
v
AggregateException
(InnerExceptions: tutti gli errori)
Perché con await non si vede AggregateException?
Con await .NET "svolge" il AggregateException e rilancia la prima eccezione interna (InnerExceptions[0]).
// Solo un task che lancia InvalidOperationException
try
{
await Task.Run(() => throw new InvalidOperationException("Errore!"));
}
catch (Exception ex)
{
// Qui ex è proprio InvalidOperationException, non AggregateException
Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}
Se vuoi accedere a tutti gli errori — usa Wait() o Result (ma ricorda il blocco del thread).
3. Catturare e gestire tutte le eccezioni
Colleghiamo la teoria alla pratica. Supponiamo che più task elaborino dati utente e tu debba registrare tutti i fallimenti.
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("Operazione non valida!")),
Task.Run(() => throw new DivideByZeroException("Divisione per zero!")),
Task.Run(() => Console.WriteLine("Il terzo task è andato a buon fine"))
};
try
{
// Aspettiamo i task in modo asincrono — otteniamo la prima eccezione interna
Task allTasks = Task.WhenAll(tasks);
await allTasks; // con await AggregateException viene svolto
}
catch (Exception ex)
{
// Con await vedremo la prima eccezione interna
Console.WriteLine($"Primo errore: {ex.GetType().Name} - {ex.Message}");
// Se è AggregateException — possiamo scorrere tutte
if (ex is AggregateException aggEx)
{
foreach (var inner in aggEx.InnerExceptions)
{
Console.WriteLine($"Errore nella lista: {inner.GetType().Name} - {inner.Message}");
}
}
}
// Ora catturiamo tutti gli errori in modo sincrono
try
{
var allTasks = Task.WhenAll(tasks);
allTasks.Wait(); // blocca il thread — non farlo in UI!
}
catch (AggregateException aggEx)
{
foreach (var e in aggEx.InnerExceptions)
{
Console.WriteLine($"[Wait] Errore: {e.GetType().Name} — {e.Message}");
}
}
}
}
}
Con await nel blocco try/catch vedrai solo il primo errore. Con Wait() arriverà il classico AggregateException con tutte le eccezioni annidate.
4. Gestione degli errori di massa
Nelle applicazioni reali spesso serve trattare molti errori in modi diversi. Per esempio, mandi email a tutti gli utenti e alcune spedizioni falliscono:
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("Errore invio email: " + ex.Message);
else
Console.WriteLine("Errore sconosciuto: " + ex.Message);
}
}
Così registri ogni fallimento e puoi intraprendere azioni individuali.
5. Suggerimenti utili
Motivi di questo comportamento
.NET privilegia la praticità: in UI di solito basta la prima eccezione (con await). Ma sul server o in sistemi dove è importante gestire tutte le fallite, AggregateException è insostituibile.
Pattern: Flatten — srotolare la lista di errori
Il metodo Flatten() trasforma AggregateException annidate in una lista piatta:
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.Flatten().InnerExceptions)
{
Console.WriteLine($"Errore srotolato: {ex.GetType().Name} — {ex.Message}");
}
}
Comportamento di Task con diversi modi di attesa
| Come aspetti il task | Quale eccezione cattureremo |
|---|---|
|
La prima eccezione interna |
|
Sempre AggregateException |
|
Sempre AggregateException |
| Await individuale | Eccezione lanciata dal task, o la prima eccezione interna se il task è terminato con AggregateException |
Importante ricordare
- AggregateException è utile in tutti gli scenari dove possono esserci molti errori contemporanei.
- Per await è comodo catturare la prima eccezione; per l'attesa sincrona — scorrere tutte le InnerExceptions.
- Usa Flatten() e Handle() per una gestione ordinata.
- Non mescolare attese sincrone e asincrone in UI: otterrai freeze e deadlock.
7. Errori tipici nella gestione di AggregateException
Mito: await Task.WhenAll lancia sempre AggregateException. In realtà no: l'eccezione viene svolta e vedrai la prima interna.
Accedere a Result o chiamare Wait() blocca il thread fino al termine dei task. In UI questo porta a interfacce bloccate.
Se vuoi gestire alcune eccezioni "silenziosamente", usa Handle():
catch (AggregateException aggEx)
{
aggEx.Handle(ex =>
{
if (ex is InvalidOperationException)
{
// Abbiamo gestito questo errore, non deve essere rilanciato
return true;
}
return false; // Le altre eccezioni saranno rilanciate
});
}
GO TO FULL VERSION