CodeGym /Corsi /C# SELF /Più eccezioni: AggregateEx...

Più eccezioni: AggregateException

C# SELF
Livello 61 , Lezione 1
Disponibile

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
await Task.WhenAll()
La prima eccezione interna
Task.WhenAll().Wait()
Sempre AggregateException
Task.WhenAll().Result
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
    });
}
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION