CodeGym /Kurse /C# SELF /Mehrere Ausnahmen: Aggrega...

Mehrere Ausnahmen: AggregateException

C# SELF
Level 61, Lektion 1
Verfügbar

1. Einführung

Stell dir vor: du startest gleich ein Dutzend asynchroner Tasks — du parst Websites, prüfst Preise auf Marketplaces oder berechnest große Datenmengen. Alles startet flott, aber mehrere Tasks stürzen mit Fehlern ab: irgendwo antwortet der Server nicht, irgendwo sind die Daten kaputt. Im synchronen Code würdest du eine Ausnahme sehen, im asynchronen können es mehrere Fehler sein. Hier kommt die AggregateException ins Spiel, die alle Fehler in einem „Container“ sammelt.

Was ist eine AggregateException?

AggregateException ist ein spezieller Exception-Typ in .NET, der Fehler aus parallelen/asynchronen Operationen aggregiert: Task.WhenAll, Parallel.For, TPL usw. Wenn zwei oder mehr Tasks mit Fehlern enden, werden sie in ein einzelnes AggregateException-Objekt „gepackt“.

Wo tritt die AggregateException auf?

Das häufigste Szenario ist das Warten auf mehrere Tasks via Task.WhenAll:

// Beispiel — wir rufen mehrere Tasks parallel auf, einige davon werfen eine Ausnahme
var tasks = new List<Task>
{
    Task.Run(() => throw new InvalidOperationException("Fehler #1")),
    Task.Run(() => throw new ArgumentException("Fehler #2")),
    Task.Run(() => { Console.WriteLine("Task 3 wurde erfolgreich ausgeführt!"); })
};

try
{
    await Task.WhenAll(tasks); // Beim await wird hier AggregateException „aufgefaltet“
}
catch (Exception ex)
{
    Console.WriteLine($"Ein Fehler ist aufgetreten: {ex.GetType().Name} - {ex.Message}");
}

Aber es gibt einen Kniff: bei await wird die AggregateException „aufgefaltet“ und es wird die erste innere Exception „wie sie ist“ geworfen. Wenn du hingegen kein await benutzt, sondern auf Result zugreifst oder Wait() aufrufst, erhältst du tatsächlich die AggregateException.

2. Wie sieht eine AggregateException aus?

Das ist eine Exception mit einer Sammlung InnerExceptions — allen eingebetteten Fehlern:

try
{
    Task task = Task.WhenAll(tasks);
    task.Wait(); // Hier wird genau eine AggregateException geworfen, falls Fehler auftraten
}
catch (AggregateException aggEx)
{
    Console.WriteLine($"Insgesamt Fehler: {aggEx.InnerExceptions.Count}");
    foreach (var e in aggEx.InnerExceptions)
    {
        Console.WriteLine($"Typ: {e.GetType().Name}, Nachricht: {e.Message}");
    }
}

Beispielausgabe:

Insgesamt Fehler: 2
Typ: InvalidOperationException, Nachricht: Fehler #1
Typ: ArgumentException, Nachricht: Fehler #2

Wie entstehen AggregateException


+--------------------------+
| Wir starten mehrere      |
|   Tasks                  |
+-----+---------+----------+
      |         |
      v         v
  Task1    Task2 ... TaskN
   |          |
   |  (wirft eine Exception)
   |-------------------+
   |                   v
   |              Exception2
   v
Exception1

   |
   v
+---------------------------------+
| Task.WhenAll oder Parallel.For  |
|        (sammelt alle            |
|         Ausnahmen)              |
+---------------------------------+
              |
              v
     AggregateException
   (InnerExceptions: alle Fehler)

Warum sieht man bei await keine AggregateException?

Bei await „faltet“ .NET die AggregateException auf und wirft die erste innere Exception (InnerExceptions[0]).

// Nur ein Task, der InvalidOperationException wirft
try
{
    await Task.Run(() => throw new InvalidOperationException("Fehler!"));
}
catch (Exception ex)
{
    // Hier ist ex genau InvalidOperationException, nicht AggregateException
    Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}

Wenn du auf alle Fehler zugreifen willst — benutze Wait() oder Result (denk aber an mögliche Thread-Blocking-Probleme).

3. Alle Ausnahmen fangen und verarbeiten

Kommen wir zur Praxis. Angenommen, mehrere Tasks verarbeiten Benutzerdaten und du willst alle Fehlschläge protokollieren.

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("Nicht die richtige Operation!")),
                Task.Run(() => throw new DivideByZeroException("Division durch Null!")),
                Task.Run(() => Console.WriteLine("Die dritte Task war erfolgreich"))
            };

            try
            {
                // Wir warten asynchron auf die Tasks — bekommen die erste innere Exception
                Task allTasks = Task.WhenAll(tasks);
                await allTasks; // bei await wird AggregateException aufgefaltet
            }
            catch (Exception ex)
            {
                // Bei await sehen wir die erste innere Exception
                Console.WriteLine($"Erster Fehler: {ex.GetType().Name} - {ex.Message}");

                // Wenn es eine AggregateException ist — kann man über alle iterieren
                if (ex is AggregateException aggEx)
                {
                    foreach (var inner in aggEx.InnerExceptions)
                    {
                        Console.WriteLine($"Fehler aus der Liste: {inner.GetType().Name} - {inner.Message}");
                    }
                }
            }

            // Jetzt fangen wir alle Fehler synchron
            try
            {
                var allTasks = Task.WhenAll(tasks);
                allTasks.Wait(); // blockiert den Thread — mach das nicht im UI!
            }
            catch (AggregateException aggEx)
            {
                foreach (var e in aggEx.InnerExceptions)
                {
                    Console.WriteLine($"[Wait] Fehler: {e.GetType().Name} — {e.Message}");
                }
            }
        }
    }
}

Bei await im try/catch-Block siehst du nur den ersten Fehler. Bei Wait() kommt die klassische AggregateException mit allen eingebetteten Fehlern.

4. Verarbeitung vieler Fehler

In realen Anwendungen muss man oft viele Fehler unterschiedlich behandeln. Zum Beispiel schickst du E-Mails an alle Nutzer und einige Sendungen schlagen fehl:

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("Fehler beim Senden der E-Mail: " + ex.Message);
        else
            Console.WriteLine("Unbekannter Fehler: " + ex.Message);
    }
}

So protokollierst du jeden Fehlschlag und kannst individuelle Maßnahmen ergreifen.

5. Nützliche Feinheiten

Gründe für dieses Verhalten

.NET entscheidet sich aus Komfortgründen so: im UI reicht meist die erste Fehlermeldung (bei await). Auf Servern oder in Systemen, in denen es wichtig ist, alle Fehlschläge zu behandeln, ist die AggregateException unverzichtbar.

Pattern: Flatten — die Fehlerliste aufklappen

Die Methode Flatten() verwandelt verschachtelte AggregateExceptions in eine flache Liste:

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

Wie verhält sich ein Task bei verschiedenen Warteweisen

Wie wir auf den Task warten Welche Exception wir fangen
await Task.WhenAll()
Erster innerer Fehler
Task.WhenAll().Wait()
Immer AggregateException
Task.WhenAll().Result
Immer AggregateException
Individuelles await Die von der Task geworfene Exception, oder der erste innere Fehler, falls die Task mit AggregateException endet

Wichtig zu merken

  • AggregateException ist nützlich in allen Szenarien, in denen gleichzeitig viele Fehler auftreten können.
  • Bei await ist es praktisch, die erste Fehlermeldung zu fangen; beim synchronen Warten sollte man alle InnerExceptions durchgehen.
  • Verwende Flatten() und Handle() für saubere Verarbeitung.
  • Mische nicht synchrone und asynchrone Warteweisen im UI: das führt zu Hängern und Deadlocks.

7. Typische Fehler bei der Verarbeitung von AggregateException

Mythos: await Task.WhenAll wirft immer eine AggregateException. Tatsächlich wird die Exception aufgefaltet und du siehst die erste innere Exception.

Der Zugriff auf Result oder der Aufruf von Wait() blockiert den Thread bis zum Abschluss der Tasks. Im UI führt das zu eingefrorener Oberfläche.

Wenn du manche Fehler „leise“ behandeln willst, benutze Handle():

catch (AggregateException aggEx)
{
    aggEx.Handle(ex =>
    {
        if (ex is InvalidOperationException)
        {
            // Diese Exception haben wir behandelt, sie muss nicht weitergeworfen werden
            return true;
        }
        return false; // Die übrigen Exceptions werden wieder geworfen
    });
}
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION