CodeGym /Kurse /C# SELF /Fehlerbehandlung in asynchronem Code

Fehlerbehandlung in asynchronem Code

C# SELF
Level 59 , Lektion 4
Verfügbar

1. Asynchrone Methoden und Ausnahmen

Wir sind es gewohnt, dass wenn im Code etwas Schlechtes passiert (z.B. Division durch Null oder der Versuch, auf eine nicht vorhandene Datei zuzugreifen), eine Ausnahme geworfen wird, die wir mit try-catch fangen können. Das ist einfach, solange der Code sequentiell und im selben Thread läuft. Sobald jedoch Asynchronität auftaucht, wird die Welt wie der offene Weltraum: eine Ausnahme kann weit entfernt vom Ort auftreten, an dem wir sie erwarten, oder überhaupt unbemerkt bleiben.

Der Grund ist, dass eine asynchrone Methode häufig eine Task (Task) zurückgibt, deren Ausführung NACH dem Verlassen der Methode weiterläuft. Die Ausnahme kann auftreten, nachdem der Hauptthread die Ausführung "losgelassen" hat und sein eigenes Leben weiterführt. Deshalb funktioniert die gewohnte Konstruktion try-catch um den Aufruf einer asynchronen Methode nicht immer so wie im synchronen Code.

Schauen wir uns ein einfaches Beispiel an. Angenommen, in unserer Mini-Anwendung gibt es diese asynchrone Methode:

// Fragment unserer Anwendung: asynchrone Berechnung "Report senden"
public async Task SendReportAsync()
{
    // Hier könnten Netzwerkaufrufe oder Dateizugriffe stehen
    await Task.Delay(100);
    throw new InvalidOperationException("Fehler beim Senden des Berichts!");
}

Und so können wir sie aufrufen:

SendReportAsync();
Console.WriteLine("Wir arbeiten weiter...");

Visualisierung

flowchart TD
    Start["Hauptthread"]
    Call[/"Aufruf von SendReportAsync()"/]
    Continue["Arbeit geht weiter..."]
    Exception["Ausnahme tritt in Task auf"]
    Unhandled["Fehler nicht behandelt!"]
    Start --> Call --> Continue
    Call -.- Exception --> Unhandled

Fazit: wenn eine asynchrone Methode eine Task zurückgibt und Sie nicht auf das Ende der Task warten (await oder .Wait()), bleibt die Ausnahme "unbemerkt". Im besten Fall schreibt die Laufzeit etwas in die Logs wie "Unhandled exception in task". Im schlimmsten Fall verlieren Sie den Fehler komplett und suchen lange nach der Quelle der "mysteriösen Bugs".

2. Wie fängt man Ausnahmen im asynchronen Code richtig?

Verwende await + try-catch

Betrachten wir den richtigen Weg:

try
{
    await SendReportAsync(); // Warten auf Abschluss der Task
    Console.WriteLine("Bericht erfolgreich gesendet!");
}
catch (Exception ex)
{
    Console.WriteLine($"Ups! Etwas ist schiefgelaufen: {ex.Message}");
}

Wie funktioniert das? Wenn Sie await vor den Aufruf einer asynchronen Methode setzen, zerlegt C# Ihre Methode in zwei Teile: vor und nach dem await. Tritt in dem asynchronen Teil eine Ausnahme auf, "springt" sie an die Stelle mit dem await und kann dort mit dem klassischen try-catch gefangen werden.

Beispiel für die Anwendung

Fügen wir Fehlerbehandlung für das Senden des Berichts in unser Demo ein:

public async Task StartReportProcessAsync()
{
    try
    {
        await SendReportAsync();
        Console.WriteLine("Bericht erfolgreich gesendet!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Fehler beim Senden des Berichts: {ex.Message}");
    }
}

Und aufrufen:

await StartReportProcessAsync();

.Wait(), .Result — keine ideale, aber funktionale Taktik für Konsolen

Manchmal, besonders in Konsolenanwendungen, kann man await auf oberster Ebene nicht verwenden (ältere C#-Versionen, Methode Main). Dann bleibt nur synchrones Warten mittels .Wait() oder .Result.

try
{
    SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
        Console.WriteLine($"Fehler: {ex.Message}");
}

Warum so? Aufrufe von .Wait() und .Result wickeln die ursprüngliche Ausnahme immer in eine AggregateException. Das ist ein Container, der eine oder mehrere Ausnahmen enthalten kann. Darin kann ein (oder mehrere!) innere Ausnahmen sein, daher muss man sie mit einer Schleife auseinandernehmen. Mehr zu AggregateException steht in der offiziellen Dokumentation.

Wichtig!

In modernen .NET-Versionen (ab C# 7.1) können Sie ein asynchrones Main deklarieren und await direkt im Entry-Point verwenden:

static async Task Main(string[] args)
{
    await StartReportProcessAsync();
}

3. Ausnahmen in "fire-and-forget"-Tasks

Was passiert, wenn Sie eine asynchrone Methode starten, ohne auf ihren Abschluss zu warten und ohne die Task-Referenz zu speichern?

SendReportAsync(); // Task "vergessen"

In so einer Situation entsteht das Problem: eine in der Task aufgetretene Ausnahme wird von niemandem behandelt. Manchmal (je nach Umgebung und Einstellungen) kann die Anwendung sogar abstürzen. Manchmal wird nur eine Warnung geloggt. Das ist kein Fehler von C#, sondern Folge der Funktionsweise von Tasks.

Wie macht man es richtig?

  • Im Idealfall: benutze niemals "fire-and-forget", wenn du dir nicht sicher bist, dass die Task nicht abstürzen kann.
  • Wenn die asynchrone Methode wirklich "fire-and-forget" sein soll, dann implementiere explizite Fehlerbehandlung innerhalb der Methode.
public async Task SendReportSafeAsync()
{
    try
    {
        await Task.Delay(100);
        throw new InvalidOperationException("Fehler beim Senden!");
    }
    catch (Exception ex)
    {
        // Loggen oder Fehler behandeln
        Console.WriteLine($"[Log] Ausnahme: {ex.Message}");
    }
}

// Aufruf
SendReportSafeAsync();

Generelle Empfehlung: Wenn eine Task von niemandem überwacht wird und Sie kein await verwenden, wickle den Körper der asynchronen Methode unbedingt in ein try-catch. So verlierst du den Fehler nicht und kannst ihn zumindest loggen.

4. Ausnahmen und parallele Tasks: Task.WhenAll und Co.

In realen Anwendungen muss man oft mehrere unabhängige asynchrone Tasks parallel starten und auf deren Abschluss warten. Zum Beispiel, wenn du Berichte an mehrere Empfänger parallel sendest:

var tasks = new List<Task>
{
    SendReportAsync(),
    SendReportAsync(),
    SendReportAsync()
};

await Task.WhenAll(tasks);

Was passiert, wenn eine (oder mehrere) Tasks eine Ausnahme werfen?

Wie fängt man solche Fehler?

Bei Verwendung von await mit Task.WhenAll(tasks) — wenn mindestens eine Task mit Fehler beendet wird, wird await die Ausnahme der zuerst fertig gewordenen fehlerhaften Task weiterwerfen (sie wird nicht in eine AggregateException verpackt).
Aber ein Hinweis: wenn mehrere Tasks fehlgeschlagen sind, dann wird eine AggregateException mit den inneren Ausnahmen geworfen.

try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // Falls es eine AggregateException ist — auseinandernehmen
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
            Console.WriteLine($"Fehler in Task: {inner.Message}");
    }
    else
    {
        Console.WriteLine($"Fehler: {ex.Message}");
    }
}

Für await mit einer einzelnen Task wird die Ausnahme normalerweise nicht in eine AggregateException eingepackt. Bei WhenAll kann das jedoch durchaus passieren!

5. Asynchrone Delegates und Fehlerbehandlung

In UI-Anwendungen (WPF, WinForms, ASP.NET) werden Event-Handler oft als asynchrone Lambdas geschrieben. Wenn eine Ausnahme in so einem Handler "nach außen" tritt, hängt das Ergebnis vom UI-Framework ab: die Anwendung kann abstürzen oder den Fehler verschlucken.

Empfehlung

Setze immer ein try-catch innerhalb asynchroner Delegates:

button.Click += async (sender, args) =>
{
    try
    {
        await SendReportAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Fehler: {ex.Message}");
    }
};
1
Umfrage/Quiz
Asynchrones Programmieren, Level 59, Lektion 4
Nicht verfügbar
Asynchrones Programmieren
Asynchronität vs. Multithreading
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION