C#: Exception Handling bei asynchronen Methoden (async/await)

Bei der Verwendung von async/await können Methoden drei unterschiedliche Rückgabetypen besitzen: Task, Task<T> oder void. In den Best Practices wird jetzt immer geschrieben: „Vermeide den Rückgabetyp void und gib immer ein Task-Objekt zurück!“ Doch warum ist das so? Das soll jetzt im nachfolgenden Artikel etwas genauer erläutert werden.

Wie in der Einleitung schon geschrieben können asynchrone Methoden nur die folgenden Rückgabetypen besitzen:

  • Task
  • Task<T> für einen beliebigen Typ T
  • void

Im Normalfall sollte man immer Task oder Task<T> verwenden, void sollte als Rückgabetyp wenn möglich vermieden werden. Dazu gleich mehr. Konvertiert man eine synchrone Methode, die einen beliebigen Typ T zurückliefert würde die asynchrone Methode Task<T> als Rückgabetyp besitzen. Eine synchrone Methode mit dem Rückgabetyp void würde hingegeben ein Task-Objekt zurückliefern. Dazu mal das folgende Codebeispiel:

Doch warum sollte man jetzt immer Task oder Task<T> verwenden? Im Grunde ist das ganz einfach: Asynchrone Methoden sind zum Rückgabezeitpunkt im Normalfall noch nicht abgeschlossen. Typischerweise wartet (await) eine asynchrone Methode auf eine lang laufende Operation. Dabei erhält der Aufrufer der asynchronen Methode die Kontrolle zurück, während die asynchrone Methode auf die Fertigstellung des neuen Threads wartet und dann ihre eigene Ausführungsfolge nach dem await in diesem Thread fortsetzt. Damit jetzt aber „gewartet“ werden kann muss der Rückgabetyp der asynchronen Methode eine sogenannte „Awaitable“-Klasse sein. Bei Task oder Task<T> handelt es sich um eine solche „Awaitable“-Klasse. Eine asynchrone Methode gibt den Rückgabewert also nicht direkt zurück, sondern verpackt diesen in ein Task-Objekt. Ebenso verhält es sich bei auftretenden Exceptions, diese werden ebenfalls in das zurückgegebene Task-Objekt verpackt.

Auf void hingegen kann nicht gewartet werden. Das heißt, eine Methodensignatur wie

public async void DoWorkAsync()

ist erlaubt, aber lässt sich nicht mit await aufrufen. Daher kommt async void typischerweise nur bei Ereignisbehandlungsroutinen zum Einsatz, die immer void liefern müssen. Da es hier keinen Rückgabetyp gibt steht man vor einem weiteren Problem: Wohin mit der Exception? Dazu aber später mehr. Kommen wir zunächst async Task Methoden.

Excpetion Handling für async Task Methoden

Schauen wir uns zunächst mal ein Beispiel für async Task Methoden an:

Innerhalb der asynchronen Methode ThrowExceptionAsync wird eine Exception vom Typ InvalidOperationException ausgelöst. In der Methode TestAsyncException wird nun mit await (Zeile 14) auf die asynchrone Methode gewartet und genau dort tritt die Exception dann auf. Hinweis: Normalerweise könnte man Zeile 9 und Zeile 14 in einem Statement schreiben (await ThrowExceptionAsync). Hier wurde es beispielhaft aufgesplittet um das Task-Objekt besser im Debugger zeigen zu können. Innerhalb des Debuggers sieht das dann so aus:

Task-Objekt in der Debugger-Ansicht

Task-Objekt in der Debugger-Ansicht

Wie im Debugger zu sehen wird die geworfene InvalidOperationException im Task-Objekt verpackt. Innerhalb der Task-Klasse gibt eine Eigenschaft Exception vom Typ AggregateException. Die AggregateException wird verwendet, um mehrere Fehler in ein einzelnes auslösbares Ausnahmeobjekt zu konsolidieren. Hier mal die Eigenschaften der Task-Klasse im Überblick:

Eigenschaften der Task-Klasse (Quelle: MSDN)

Eigenschaften der Task-Klasse (Quelle: MSDN)

Excpetion Handling für async void Methoden

Bei async void Methoden ist das oben beschriebene Verhalten nicht möglich, da es ja kein Rückgabeobjekt gibt, in welches die Exception verpackt werden kann. Daher wird in diesem Fall immer die folgende Herangehensweise empfohlen: Wann immer möglich sollte der Rückgabetyp von void in Task geändert werden. In manchen Fällen ist das aber nicht immer möglich, z.B. bei Event-Handlern, die immer void zurückliefern müssen. Hier sollte man den Code innerhalb der async void Methode in einen try-Block packen und die Exception direkt behandeln.

Es gibt noch eine weitere Möglichkeit Exceptions innerhalb von async void Methoden zu behandeln: Wenn innerhalb einer async void Methode eine Exception auftritt wird diese Exception im SychronizationContext bereitgestellt, welcher zu dem Zeitpunkt aktiv war als die asynchrone Methode gestartet wurde. Wenn die ausführende Umgebung nun einen SychronizationContext zur Verfügung stellt dann gibt es im Normalfall auch einen Weg die aufgetretenen Top-Level-Exceptions auf globaler Ebene zu handhaben. In WPF-Applikationen gibt es z.B. das Application.DispatcherUnhandledExceptions Event (hier können diese Ausnahmen dann behandelt werden). Das könnte etwa so aussehen:

In Zeile 5 erfolgt die Registrierung auf das DispatcherUnhandledException-Event. Der Event-Handler könnte in etwa so aussehen:

Hier wird eigentliche Exception behandelt und eine Fehlermeldung ausgegeben. In Zeile 8 wird das Handled-Flag auf true gesetzt um zu verhindern, dass die Applikation geschlossen wird.

Fazit

Um Exceptions nun sicher zu handeln sollte man folgendes beachten:

  • Entweder man behandelt die auftretenden Exceptions direkt in der asynchronen Methode oder
  • man gibt ein Task– bzw. Task<T>-Objekt zurück und stellt sicher, dass der Aufrufer evtl. auftretende Exceptions handelt

Literaturverzeichnis und Weblinks

Abk.Quelle
[1]Async/Await - Best Practices in Asynchronous Programming
https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
[2]Async & exceptions in C#
https://blogs.msdn.microsoft.com/ptorr/2014/12/10/async-exceptions-in-c/
[3]Gewusst wie: Behandeln von Ausnahmen, die von Aufgaben ausgelöst werden
https://msdn.microsoft.com/de-de/library/dd537614(v=vs.110).aspx

leave your comment

Fork me on GitHub