Menu

C# try catch: Exceptions mit catch, when und finally behandeln

try, catch und finally sind die Art, wie C# Exceptions behandelt. Lerne, wie eine Exception den Aufrufstapel hinaufwandert, wie du bestimmte Exception-Typen in der richtigen Reihenfolge abfängst, mit when filterst, in finally aufräumst, mit throw; erneut wirfst, ohne den Stacktrace zu verlieren, und welche Exceptions du nicht abfangen solltest.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Wenn zur Laufzeit etwas schiefgeht (eine Datei fehlt, ein Text ist keine Zahl, ein Schlüssel steht nicht in einem Dictionary), wirft .NET eine Exception: ein Objekt, das den Fehler beschreibt. Die Exception wandert den Aufrufstapel hinauf, bis ein catch-Block sie behandelt. Tut das keiner, stoppt das Programm und gibt den Fehler aus.

Ein einfaches try catch

Schließe den Code, der scheitern kann, in try ein und behandle den Fehlschlag in catch:

Ausgabe:

42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running

Wenn int.Parse("forty-two") wirft, wird das Console.WriteLine danach im try-Block übersprungen, der Block catch (FormatException) läuft, und die Schleife geht weiter. Du kannst die Variable weglassen (catch (FormatException)), wenn du das Exception-Objekt nicht brauchst.

Für diesen Fall gibt es ein besseres Werkzeug: int.TryParse(input, out int n) gibt false zurück, statt zu werfen. Exceptions sind für Situationen, die der Code nicht erwartet; Eingaben, die oft ungültig sind, sind erwartbar, prüfe sie also, statt Exceptions abzufangen.

Wie eine Exception wandert

Eine Exception, die tief in einer Aufrufkette geworfen wird, baut jede Methode ab, bis sie einen passenden Handler findet. Code nach dem Wurf läuft in keiner dieser Methoden.

Ausgabe:

OrderTotal finished
5.00
Caught KeyNotFoundException in Main

PriceOf und OrderTotal haben kein catch, die Exception läuft also direkt durch sie hindurch zu Main. Setze einen Handler auf die Ebene, die weiß, was bei dem Fehlschlag zu tun ist, und das ist oft nicht die Stelle, an der er passiert ist.

Bestimmte Exceptions abfangen, in der richtigen Reihenfolge

Eine catch-Klausel behandelt ihren Typ und jeden davon abgeleiteten Typ. Bei mehreren Klauseln gewinnt die erste passende, ordne sie also vom Spezifischsten zum Allgemeinsten:

Ausgabe:

10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException

Der letzte Aufruf wirft OverflowException, die keine der spezifischen Klauseln behandelt, also übernimmt das allgemeine catch (Exception e). catch (Exception) zuerst zu setzen würde die folgenden Klauseln unerreichbar machen, und der Compiler lehnt das mit Fehler CS0160 ab.

Exception-Filter mit when

C# 6 hat when hinzugefügt: eine Bedingung, die entscheidet, ob eine catch-Klausel greift. Ist sie false, sucht die Runtime weiter nach einem anderen Handler, als gäbe es die Klausel nicht.

Ausgabe:

Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401

Mit Filtern verzweigst du nach Daten in der Exception, ohne abzufangen und erneut zu werfen. Sie sind auch der saubere Weg, zwei voneinander unabhängige Typen gleich zu behandeln, wie es die dritte Klausel tut. Ein Filter läuft, bevor der Stack abgebaut wird, ein Debugger oder Crash-Dump zeigt also noch den ursprünglichen Zustand, wenn kein Filter passt.

finally: Code, der immer läuft

Ein finally-Block läuft, wenn die Kontrolle das try verlässt, egal ob es fertig wurde, früh zurückkehrte oder warf. Dort gehört das Aufräumen hin. (Eine Einschränkung: Wenn nirgends etwas die Exception abfängt, kann der Prozess enden, ohne ihn auszuführen.)

Ausgabe:

Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error

In jedem Fall wird „Close connection“ ausgegeben, bevor das Ergebnis der Methode Main erreicht: Das finally läuft, nachdem der return-Wert berechnet wurde, aber bevor die Methode tatsächlich zurückkehrt. Ein try kann ein finally ganz ohne catch haben, das aufräumt und die Exception trotzdem zum Aufrufer weiterlaufen lässt.

Für Objekte, die IDisposable implementieren (Dateien, Streams, Verbindungen), schreibt die using-Anweisung dieses try/finally für dich.

Erneut werfen: throw; vs. throw e;

Manchmal protokolliert ein catch-Block etwas und lässt die Exception dann weiterlaufen. Wie du erneut wirfst, entscheidet, ob der Stacktrace erhalten bleibt:

Ausgabe:

throw;   trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False

throw e; behandelt die Exception, als wäre sie neu aus dem catch-Block geworfen, die Frames darunter, einschließlich der Methode, in der der Fehler passiert ist, sind also aus dem Trace verschwunden. Wirf immer mit einem nackten throw; erneut. (Das Attribut NoInlining steht nur da, weil der JIT eine so kleine Methode in ihren Aufrufer einfügen könnte, was sie in beiden Traces verbergen würde.)

Um stattdessen Kontext hinzuzufügen, kapsle die Exception in eine neue und übergib die ursprüngliche als innere Exception: throw new ConfigException("Could not start the app", e);. Die innere Exception behält ihren eigenen Stacktrace, und Logger geben die Kette aus. Wie du eigene Exception-Typen schreibst, steht unter Exceptions werfen.

Das Exception-Objekt

Jede Exception leitet von System.Exception ab. Die Member, die du am häufigsten verwendest:

MemberWas er enthält
MessageEine menschenlesbare Beschreibung
GetType().NameDen Exception-Typ, etwa FormatException
StackTraceDie Kette der Methodenaufrufe an der Wurfstelle
InnerExceptionDie Exception, die diese verursacht hat, oder null
ToString()Typ, Meldung, innere Exceptions und Stacktrace zusammen

Protokolliere e.ToString() statt e.Message, wenn du einen Fehlschlag später diagnostizieren willst: Die Meldung allein sagt selten, wo das Problem lag. Zeige Endbenutzern nicht e.ToString().

Häufige Exception-Typen

ExceptionTypische Ursache
NullReferenceExceptionEinen Member auf einer null-Referenz aufrufen
ArgumentNullExceptionEiner Methode wurde null übergeben, wo sie einen Wert braucht
ArgumentOutOfRangeExceptionEin Argument oder Listenindex liegt außerhalb des erlaubten Bereichs
IndexOutOfRangeExceptionEin Array-Index liegt außerhalb seiner Grenzen
FormatExceptionint.Parse, DateTime.Parse und Co. auf Text im falschen Format
InvalidCastExceptionEin expliziter Cast in einen Typ, den das Objekt nicht hat
InvalidOperationExceptionDas Objekt ist für den Aufruf im falschen Zustand (leere Sequenz, geänderte Collection)
KeyNotFoundExceptionEinen fehlenden Dictionary-Schlüssel mit dem Indexer lesen
DivideByZeroExceptionDivision durch null bei Ganzzahlen oder decimal
OverflowExceptionEine geparste Zahl, eine geprüfte Umwandlung oder ein geprüftes Rechenergebnis passt nicht in den Typ
FileNotFoundException, IOExceptionProbleme mit dem Dateisystem

NullReferenceException, IndexOutOfRangeException und InvalidCastException bedeuten fast immer einen Bug. Korrigiere den Code, statt sie abzufangen.

Exceptions nicht verschlucken

Ein leeres catch versteckt jeden Fehler, auch die, mit denen du nicht gerechnet hast:

try
{
    SaveOrder(order);
}
catch (Exception)
{
    // nothing: the order silently was not saved
}

Das Programm läuft weiter, als hätte das Speichern funktioniert, und die eigentliche Ursache geht verloren. Richtlinien, die die Ausnahmebehandlung ehrlich halten:

  • Fang nur ab, was du behandeln kannst, auf der Ebene, die es behandeln kann.
  • Wenn du zum Protokollieren abfängst, wirf mit throw; erneut, außer das Programm kann wirklich weitermachen.
  • Fang Exception nur am äußeren Rand ab: Main, ein Request-Handler, eine Worker-Schleife.
  • Bevorzuge für erwartbare Fälle TryParse, TryGetValue und null-Prüfungen statt Exceptions. Werfen ist im Vergleich zu einer Prüfung langsam, während ein try-Block, der nichts wirft, fast nichts kostet.

Häufige Fehler

  • catch (Exception) zuerst. Spätere Klauseln werden unerreichbar (CS0160).
  • throw e; zum erneuten Werfen. Es löscht den ursprünglichen Stacktrace; nimm throw;.
  • Leere catch-Blöcke. Fehler verschwinden; protokolliere wenigstens und wirf erneut.
  • Exceptions zur Ablaufsteuerung verwenden. Validiere Eingaben mit TryParse, statt FormatException abzufangen.
  • Benutzern e.Message einer Framework-Exception zeigen. Der Wortlaut unterscheidet sich zwischen .NET-Versionen und ist für Entwickler geschrieben.

Häufig gestellte Fragen

Wie funktioniert try catch in C#?

Code, der scheitern könnte, steht im try-Block. Wirft eine Anweisung dort eine Exception, wird der Rest des Blocks übersprungen, und die Runtime sucht eine catch-Klausel, deren Typ zur Exception passt, zuerst in der aktuellen Methode und dann in jedem Aufrufer. Das erste passende catch läuft, und die Ausführung geht nach der ganzen try-Anweisung weiter.

Läuft finally in C# immer?

Fast immer: nachdem der try-Block normal endet, nachdem ein catch eine Exception behandelt hat, nach einem return oder break im Block und wenn eine Exception auf dem Weg zu einem catch weiter oben im Aufrufstapel durchläuft. Es läuft nicht, wenn der Prozess vorher endet: ein beendeter Prozess, Environment.FailFast, eine StackOverflowException und ab .NET Core eine Exception, die nichts abfängt und die den Prozess beendet, bevor finally-Blöcke laufen.

Wie fange ich in C# mehrere Exceptions ab?

Schreibe mehrere catch-Klauseln, den spezifischsten Typ zuerst: catch (FileNotFoundException) vor catch (IOException) vor catch (Exception). Der Compiler lehnt eine Klausel ab, die nie erreicht werden kann, weil eine frühere ihren Typ schon abfängt. Um zwei voneinander unabhängige Typen gleich zu behandeln, nimm einen Filter: catch (Exception e) when (e is FormatException || e is OverflowException).

Was ist der Unterschied zwischen throw und throw ex in C#?

Innerhalb eines catch wirft throw; die aktuelle Exception erneut, mit ihrem ursprünglichen Stacktrace. throw ex; wirft dasselbe Objekt, setzt den Stacktrace aber auf die aktuelle Zeile zurück, die Methode, in der der Fehler tatsächlich passiert ist, verschwindet also aus dem Trace. Nimm throw; oder kapsle die Exception: throw new MyException("context", ex);.

Was ist ein Exception-Filter in C#?

Eine when-Klausel nach einem catch (ab C# 6): catch (HttpRequestException e) when (e.Message.Contains("404")). Der catch-Block läuft nur, wenn die Bedingung true ist; sonst sucht die Exception weiter nach einem anderen Handler, als gäbe es die Klausel nicht, und ihr Stack wird nicht abgebaut.

Sollte ich in C# Exception abfangen?

Nur an den Rändern eines Programms: am Anfang von Main, in einem Request-Handler oder einer Hintergrundschleife, wo die Aufgabe ist, den Fehler zu protokollieren und weiterzumachen oder sauber zu beenden. Tief im Code fang die bestimmten Typen ab, die du tatsächlich behandeln kannst, und lass alles andere weiterlaufen.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S