In letzter Zeit wurde ich immer wieder gebeten etwas über Fehlerbehandlung zu schreiben. Da das Thema komplex und sehr umfangreich ist habe ich mich immer wieder darum gedrückt. Man sieht aber in der Praxis leider doch, dass es immer wieder Probleme damit gibt. Deshalb habe ich mich jetzt endlich aufgerafft das Thema anzugehen.
Da das Thema zu Komplex für einen einzelnen Beitrag ist, werde ich das Ganze als Serie aufbauen:
- Grundlagen der Fehlerbehandlung
- Eigene Ausnahamen werfen
- Guidelines nach David Abrahams
- Fehlerbehandlung auf Anwendungsebene
Beim Verwenden einer Try-Catch-Anweisung kann man wirklich viel falsch machen. Viel mehr als einem meistens bewusst ist. Deshalb ist es wichtig sich die Grundlagen nochmals genauer anzusehen.
Try-Anweisung
Die Try-Anweisung schließt den Code ein, der auf Ausnahmen überwacht werden soll. Der Code darin wird bis zu der Stelle ausgeführt an der ein Fehler auftritt. Dort wird die Ausführung abgebrochen. Wichtig ist dabei sich im Klaren zu sein, welchen Statusänderungen ev. nicht ausgeführt werden. Eine Try-Anweisung benötigt immer eine catch oder eine finally Anweisungen – oder beide.
object obj = null; try { int i = (int)obj; }
Catch-Anweisung
Die Catch-Anweisung wird aufgerufen, wenn im try-block eine Ausnahme aufgetreten ist. Ein catch benötigt nicht unbedingt einen Ausnahmetyp:
catch { }
In diesem Falle würde einfach bei allen Ausnahmen die Anweisung (hier nichts!) ausgeführt werden.
Wird ein Typ angegeben, so wird für jeden Ausnahmetyp die entsprechende Anweisung aufgerufen:
catch (NullReferenceException) { } catch (Exception) { }
Die Statements müssen immer von der speziellen zur allgemeineren Ausnahme gehen – ansonsten beschwert sich der Compiler. Ein variablentyp ist nicht erforderlich – um die Ausnahme zu behandeln macht es aber i.d.R. Sinn:
catch (NullReferenceException ex) { DoSomeThing(ex); } catch (Exception ex) { DoSomeThing(ex); }
Da jede Catch-Anweisung einen eigenen Scope hat kann immer derselbe Variablennamen verwendet werden.
Finally
Die Finally-Anweisung wird im Anschluss an den Code im Try-Block ausgeführt – egal ob ein Fehler auftrat oder nicht. Sie eignet sich daher zum Aufräumen (Dispose von Objekten, Löschen von temp. Dateien etc.).
finally { CleanUp(); }
Man muss darauf Achten, dass beim Auftreten eines Fehlers ev. Nicht alle Objekte initialisiert wurde. Deshalb muss immer auf NULL geprüft werden.
Fehlerbehandlung auf Klassenebene
Wenn man nicht gerade das Fehlerhandling auf Applikationsebene implementiert, dann gibt es eigentlich nur folgende Gründe eine Fehlerbehandlung zu implementieren:
- Man muss Aufräumarbeiten erledigen – auch wenn Fehler auftreten: in diesem Fall reicht ein try-finally und man kann einfach auf den catch verzichten. Die originale Ausnahme wird dann an das aufrufende Programm weitergegeben und man verliert keine Informationen aus dem StackTrace.
- Man muss Fehler loggen damit Administratoren die Ursache beseitigen können. In diesem Fall muss man den Fehler im Catch-Bereich Loggen. Da das System aber immer noch in einem Fehlerhaften Zustand ist sollte man die Ausnahme trotzdem an das aufrufende Programm weitergeben.
- Man will oder muss den Fehler mit eigenen Informationen anreichern. Zum Beispiel könnte eine “FileNotFoundException” nicht deutlich genug sein. In diesem Falle sollten man ergänzen welche Datei fehlt, wozu wir sie brauchen etc. Hier ist es wichtig den richtigen Fehler zu fangen. Es darf hier auf keinen Fall System.Exception gefangen werden. Außerdem sollte darauf geachtet werden den StackTrace nicht zu verlieren.
- Man muss einen Fehler “schlucken”, da man zum Beispiel auf eine Bedingung testet und dann einen alternativen Weg wählen. Hier ist es ebenso wichtig den richtigen Fehler zu fangen. Es darf auch hier auf keinen Fall System.Exception gefangen werden!
Rethrow
Wenn man einen Fehler weiterreichen muss (2 + 3), dann muss wir darauf Achten, dass der StackTrace erhalten bleibt. Dies erreicht man durch einen einfachen Aufruf von throw.
catch (Exception ex) { throw; }
Bitte nicht folgende machen, da dabei der Stack verloren gehen würde:
throw ex;
Wenn wir den Fehler anreichen, dann sollten wir immer die originale Exception als InnerException angeben:
catch (Exception ex) { throw new Exception("Add significans to the error.", ex); }
Soweit die Grundlagen zur Fehlerbehandlung in Klassen. Nochmal zusammengefasst:
Niemals Syste.Exception als Fehler fangen und “schlucken”!
Niemals das System nach einer Ausnahme in einem undefinierten Zustand lassen.
Keine Ausnahmen weiterleiten durch throw ex oder ohne die originale Ausnahme als InnerException anzuhängen.
Für Aufräumarbeiten nur try-finally verwenden
Immer darauf achten, dass das System nicht in einen undefinierten Zustand gerät!
Fehler durch eigene Informationen anreichern.
Weiterführende Informationen findet man auf MSDN: