Anti-Patterns: Werfen und Fangen von java.lang.Exception
February 2nd, 2010
Immer wieder entdeckt man als Java-Entwickler Code-Abschnitte in dem ein “Exception Handling” verbaut ist. Das ist erfreulich und auch gut so. Exception Handling erlaubt es auf Fehler die zur Laufzeit der Software auftreten zu reagieren. Manchmal entdeckt man dabei jedoch Code-Stücke wie:
1 2 3 | if (BEDINGUNG) { throw new Exception("An error occurred."); } |
oder
1 2 3 4 5 | try { // some code } catch (Exception e) { // exception handling } |
Diese Art mit der java.lang.Exception herumzuspringen birgt einige Nachteile und sogar Risiken. Warum Nachteile enstehen und welchen Risiken man sich aussetzt wenn man sein Exception Handling an der Basisklasse aller Exceptions ausrichtet, sei hier deshalb kurz aufgezeigt:
Lesbarkeit
Die Lesbarkeit des Codes leidet unter dem Werfen von java.lang.Exception.
1 | throw new CorruptFileException(); |
sagt deutlich mehr aus als ein einfaches
1 | throw new Exception(); |
Ich selbst vergleiche das Werfen von java.lang.Exception gerne damit jemandem versehentlich auf den Fuss zu treten. Das ist ungefähr so, als träte man jemanden versehentlich auf den Fuss und bekommt als Antwort einen Schlag ins Gesicht. Es geht einfach ein großer Teil der Information der Exception verloren.
Angst und Unsicherheit
Bei catch-Blöcken, die java.lang.Exception fangen, wird mir immer mulmig im Bauch. Es entsteht der Eindruck als wüsste der Entwickler nicht, welche Fehler in diesem Teil seiner Software auftreten können.
1 2 3 4 5 | try { // some code } catch (Exception e) { // exception handling } |
Um diese Unsicherheit zu beseitigen, wird fälschlicherweise oft auf java.lang.Exception gecatcht. Das ist schlecht. Besser ist es die Exception einfach “nach oben” durchkrachen zu lassen oder nur die Exceptions zu fangen, die der zugehörige Source-Code auch werfen kann. Ein catch auf java.lang.Exception ist im Zweifelsfall immer die schlechteste Wahl, da hier auch alle Exceptions gefangen werden, die an dieser Stelle des Source-Code besser nicht gefangen werden sollten. Noch schlimmer wird es wenn auf Throwable gecatcht wird.
1 2 3 4 5 | try { // some code } catch (Throwable t) { // exception handling } |
An dieser Stelle werden nicht nur alle Exceptions sondern auch alle Errors gefangen. Das hat dann mit Genauigkeit gar nichts mehr zu tun.
Schlechtere Wartbarkeit
Ein Entwickler, der für diese Software später vielleicht mal die Pflege und/oder Änderungen übernimmt und mit der Software nicht vertraut ist, der wird sich unter Umständen tot suchen, wenn das Logging nicht Klasse und Zeile des Sources mitloggt und einen catch wie in dem folgenden Beispiel macht.
1 2 3 4 | try { } catch (Exception e) { LOG.error(e); } |
Dieser Fehler kann insbesondere dann kritisch werden, wenn eine Software das für das Logging das Log4J PatternLayout verwendet, das gerade beim Loggen von Klasse und Zeile sehr viel Performance frisst und deshalb für das Logfile manchmal maximal noch Datum, Uhrzeit und Log-Level ins Pattern konfiguriert wird.
Verlust des Informationsgehalts
Nicht nur die Message einer Exception sondern auch der Name einer Exception kann auch einen Informationsgehalt haben und den Code leserlicher machen. So ist
1 2 3 4 5 | try { // some code } catch (PrinterUnreachableException e) { // exception handling } |
mit deutlich mehr Information bestückt als
1 2 3 4 5 | try { // some code } catch (Exception e) { // exception handling } |
RuntimeExceptions werden verschluckt
Entwickler sind auch nur Menschen und Menschen passieren Fehler. Jeder Entwickler hat schon mal einen Fehler gemacht, der zum Beispiel eine NullPointerException oder eine ähnliche RuntimeException ausgelöst hat.
1 2 3 4 5 6 7 | try { // some code Person person = null; String = person.getName(); } catch (Exception e) { LOG.error("An error occurred."); } |
Diese NullPointerException wird einfach verschlammt. Sie weißt auf einen Programmierfehler hin aber verschwindet weil sie unsachgemäß verschlammt wird. Der Entwickler, der diesen Source nun debuggen soll, hat keinen sinnvollen Anhaltspunkt was schief gelaufen sein könnte.
Erst fangen und dann doch lieber weiter werfen
1 2 3 4 5 6 7 8 | public void foo() throws Exception { try { // some code } catch (Exception e) { LOG.error(e); throw e; } } |
Dieser Fall ist auch mit einem negativen Beigeschmack versehen. Auch wenn er auf den ersten Blick vielleicht völlig korrekt erscheinen mag. Die gefangene Exception wird ins Logfile geschrieben und dann weitergeworfen. Was wird wohl der Source außerhalb Methode foo machen? Richtig! Er wird die Exception irgendwo erneut fangen und mit großer Wahrscheinlichkeit noch mal im Logfile versenken. Die Exception wird 2 Mal im Logfile versenkt obwohl sie nur ein mal aufgetreten ist. Ugly hoch drei!
Vertuschen
Leere catch-Blöcke sind genauso gefährlich wie das Fangen von java.lang.Exception. Doch noch viel schlimmer wird das Ganze, wenn eine java.lang.Exception gefangen wird und dann zusätzlich auch noch der catch-Block leer bleibt.
1 2 3 | try { // some code } catch (Exception e) {} |
Dieser Fall ist der Supergau. Der Fehler wird weder behandelt noch wird er jemals auffindbar. Er wird vertuscht und “das große Suchen” beginnt. Eine weitere Form des Vertuschens ist das Loggen der Exception-Message innerhalb catch-Blocks auf java.lang.Exception.
1 2 3 4 5 | try { // some code } catch (Exception e) { LOG.error(e.getMessage()); } |
Diese Art der Vertuschung wird manchmal damit begründet, dass das Logfile nicht zu lang werden soll und/oder der Logfile Inhalt nicht mehr schön aussieht. Wobei wir dann natürlich bei der Frage wären, ob wir das Logfile überhaupt noch brauchen. Ein Logfile das “hübsch” aussieht, das ist genauso hilfreich bei einer eventuellen Fehlersuche als wenn man die Software ohne Logfile im Blindflug betreibt. Noch eine weitere Form des Vertuschens ist das einfache returnen von null im Fehlerfall.
1 2 3 4 5 | try { // some code } catch (Exception e) { return null; } |
Diese Möglichkeit kann zwar in einigen Fällen sinnvoll erscheinen aber weißt auf alles andere als einen guten Entwicklungstil hin. Und gerade bei umfangreicheren Methoden ist die Lösung pauschal schon falsch.
Wie man jetzt sicher einfach versteht, ist das direkte arbeiten mit java.lang.Exception eher so der “Holzfäller-Style” der Programmierung. Deshalb sollte man sich neben “Fail fast! Fail early! Fail loud!” auch mit den für den jeweiligen Source-Bereich passenden Exceptions arbeiten.
In diesem Sinne…
Lasst es krachen! Aber richtig!
Für weiter Interessierte empfehle ich:
http://today.java.net/pub/a/today/2006/04/06/exception-handling-antipatterns.html

