Exception Handling

Oft sind die trivialsten Themen jene, die – wie es so schön heißt – versumpern. Und es sind auch jene Themen, die dann am Ende des Tages – durch den häufigen Einsatz – am meisten Kopfweh machen. Nach dem 100sten Bug, den man analysiert hat – akribisch – fast wie ein Detektiv – kommt zum Schluss: das hätte alles nicht so sein müssen – ich hätte mir viel Arbeit erspart, wenn es der Entwickler „besser“ gemacht hätte. Ein Thema ist: Exception Handling.

Michael Vodep

First of all: das Bild sagt schon alles. Man kann reflektieren was man will, man kann recherchieren was man will – es gibt nicht die eine, richtige Herangehensweise bzw. Lösung. Anbei, was ich mir zusammengereimt hab. Die Definition einer Exception ist recht straight forward:

  • a data structure storing information about an exceptional condition [1]
  • useful way to signal that a routine could not execute normally [1]

Die Definition lässt schon mal ahnen, dass eine Exception für einen nicht-mormalen-Flow im Programm ist. Um das zu verfeinern hilft es, sich Typen von Exceptions anzuschauen. Ich finde die Aufzählung von Eric Lippert [2] am besten:

  • Fatal exceptions: Fatal exceptions are not your fault, you cannot prevent them, and you cannot sensibly clean up from them.
    • Out of memory, thread aborted, and so on.
    • There is absolutely no point in catching these because nothing your puny user code can do will fix the problem. Just let your “finally” blocks run and hope for the best.
  • Boneheaded Exceptions: Boneheaded exceptions are your own darn fault, you could have prevented them and therefore they are bugs in your code.
    • You should not catch them; doing so is hiding a bug in your code.
    • That argument is null, that typecast is bad, that index is out of range, you’re trying to divide by zero
  • Vexing exceptions: Vexing exceptions are the result of unfortunate design decisions.
    • Vexing exceptions are thrown in a completely non-exceptional circumstance, and therefore must be caught and handled all the time.
    • E.g. Int32.Parse initial in .NET - später kam TryParse
  • Exogenous Exceptions: Exogenous exceptions appear to be somewhat like vexing exceptions except that they are not the result of unfortunate design choices.

Die Aufzählung hat eine lange Diskussion ausgelöst – aber ich finde sie nahezu genial. Was sie auf jeden Fall zeigt: Leute fangen und werfen Exceptions oft in den „falschen Momenten“ des Lebens. Dadurch stellt man sich nur selbst ein Bein. Ein User stellte eine sehr gute Frage:

When a user enters in the username and password does the login Manager throw and exception or does it offer a TryLogin?

Diese Frage würde ich gerne nachher nochmal aufgreifen, da es keine eindeutige Antwort auf Eric Lipperts Seite gab.

Wann also eine Exception werfen?

Bevor wir anfangen: Learn from another. Hier lohnt es sich meiner Meinung nach in die Welt von Java zu schauen, da Java fast ein anderes Konzept verfolgt: Checked bzw. Unchecked Exceptions.

Michael Vodep

Die Vererbungshierarchie zeigt die Zusammenhänge. Checked Exceptions kann man wie folgt erklären:

Michael Vodep

Der Aufrufer einer Methode hat den Auftrag eine Exception zu fangen (in diesem Fall IOException) oder sie weiter zu werfen (mittels throws Klausel).

Bei einer Unchecked Exception ist das nicht notwendig. Beispiele sind: ArrayIndexOutOfBoundsException, IllegalArgumentException oder NullPointerException (erben von RuntimeException). So kennen wir es in C#.

Warum kennt C# diese Konzepte nicht? Dazu hat sich der Architekt der Sprache - Anders Hejlsberg – selbst geäußert.

The Trouble with Checked Exceptions
A Conversation with Anders Hejlsberg, Part II
by Bill Venners with Bruce Eckel
August 18, 2003
https://www.artima.com/intv/handcuffs.html

Zusammengefasst:

  • Adding a new exception to a throws clause in a new version breaks client code. It’s like adding a method to an interface.
  • The trouble begins when you start building big systems where you’re talking to four or five different subsystems. Each subsystem throws four to ten exceptions. Now, each time you walk up the ladder of aggregation, you have this exponential hierarchy below you of exceptions you have to deal with. You end up having to declare 40 exceptions that you might throw….

Wenn man Dr. Google bemüht, so wird man auch schnell sehen, dass das Thema „Checked Exceptions“ Tonnen an Diskussionen fabriziert hat. Unter anderem auch folgende aus Wikipedia [3] – ich hab mir das Paper teilweise durchgelesen – bildet euch am besten eure eigene Meinung drüber – ich betrachte die Aussage mit Vorsicht – aber sie ist auf jeden Fall erwähnenswert:

Programming languages differ substantially in their notion of what an exception is. Contemporary languages can roughly be divided into two groups:

  • Languages where exceptions are designed to be used as flow control structures: Ada, Java, Modula-3, ML, OCaml, Python, and Ruby fall in this category.
  • Languages where exceptions are only used to handle abnormal, unpredictable, erroneous situations: C++, C#, Common Lisp, Eiffel, and Modula-2.

Um die Aussage noch etwas zu verfeinern – ein Auszug aus der Java Bibel [4]:

  • “Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
  • “By confronting the API user with a checked exception, the API designer presents a mandate (“Auftrag”) to recover from the condition. The user can disregard (“missachten”) the mandate by catching the exception and ignoring it, but this is usually a bad idea.”

Und ich glaube mit dieser Aussage kann man sich ein Bild malen: In Java setzt man Exceptions auch für Situationen ein, wo ich als Aufrufer sicherstellen kann, dass ich den State wieder in einen konsistent Zustand bringen kann. Das stimmt auch teilweise mit der Aussage aus dem referenzierten Paper [5] überein, wo von partial oder total failures die Rede ist. Wir wollen das Bild hier in einem unfertigen Zustand lassen - nehmen aber die wichtigsten Eckpunkte in unsere Überlegung mit: Recoverable State.

Das erste Antipattern: Exceptions as Flow-Control [6]

try
{
  for (int i = 0; /*wot no test?*/ ; i++) array[i]++;
}
catch (ArrayIndexOutOfBoundsException e) {} 

Wo ist das Problem? Sonar schreibt das sehr schön:

Using Exceptions as flow control leads to GOTOish code and obscures true exceptions when debugging.

Wie wir aus Java mitgenommen haben: wenn wir eine Exception fangen, sollten wir auch sicherstellen, dass der State wieder konsistent ist. Wenn ich jetzt in eine 10-Zeilen Methode bin (mit einem if / else) und es kracht in Zeile 6 – soll es an mir liegen – aber ich muss da immer 3 Mal hinschauen, um festzustellen, wie ich bereits getätigtes kompensieren kann – bzw. was ist schon passiert und was hätte noch passieren sollen (Zeile 7-10). Die Exception springt aus der aktuellen Ausführung heraus – wie ein „GOTO“.

“When implementing your own methods, you should throw an exception when the method cannot complete its task as indicated by its name.” [CLR via C#]

Ich möchte die Frage von oben nochmal aufgreifen:

When a user enters in the username and password does the login Manager throw and exception or does it offer a TryLogin?

Variante 1: Login kann gut gehen oder nicht - daher LoginResult zurückgeben mit „LoginSuccessful“, „PasswordExpired“, … Meiner Meinung nach ist das sehr gut zu lesen was ausgeführt wird – und was eben nicht.

var loginResult = LoginManager.Login(userName, password); 

if(loginResult.Status == LoginStatus.Successful)
{ 
  RedirectToApp(loginResult.SessionToken); 
} 

if(loginResult.Status == LoginStatus.WrongCredentials) 
{
  InformUserOrPasswordWrong(); 
}

if(loginResult.Status == LoginStatus.PasswordExpired)
{ 
  RedirectToPasswordExpired(loginResult.User); 
} 

Variante 2: Login schmeißt eine Exception wenn Login nicht passt.

try 
{
  var sessionToken = LoginManager.Login(userName, password);
  
  RedirectToApp(sessionToken);
  
  // This code will not be executed > < 
  // ...
}
catch (WrongCredentialsException exception) 
{ 
  InformUserOrPasswordWrong(); 
}
catch (PasswordExpiredException exception) 
{ 
  RedirectToPasswordExpired(exception.User); 
} 

Meiner Meinung nach schwieriger zu lesen. Der Code wird teilweise nicht mehr ausgeführt. Er „springt“ wie ein GOTO in den catch-Handler.

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

versus

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Dieses Beispiel hab ich auf Stackoverflow gefunden und zeigt die Pros für Return Codes. Meiner Meinung nach ist der Linke Teil viel besser zu lesen und er bildet vor allem die Business-Logik viel besser ab. Was man allerdings nicht machen darf: mit mehreren returns aus dem Flow herausspringen. Was sind die Nachteile von Return-Codes?

  • Return codes are more brittle: Man kann sie ignorieren, was später zu einem Problem führen kann
  • Method-Chaining geht nicht
  • Exception are typed: “You can send different classes for each kind of exception … Each catch can then be specialized.”

Siehe auch:

Ein interessantes Zitat auch aus „Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries” – hier spricht man sich gegen Return-Codes aus:

“Exceptions integrate well with object-oriented languages. … For example, in the case of constructors, operator overloads, and properties, the developer has no choice in the return value. For this reason, it is not possible to standardize on return-value-based error reporting for object-oriented frameworks. An error reporting method, such as exceptions, which is out of band of the method signature is the only option.

Hier finde ich aber, dass ein zu starkes Schwarz-Weiß denken stattfindet – man sollte sich nicht nur für Die eine Methode entscheiden – sondern sie im richtigen Kontext einsetzen.

Mein Fazit

  • Wenn ich versuche Business Logik abzubilden, dann verwende ich Return Codes / Check Methods
    • Es gibt eine Anforderung „Wenn der Benutzer ein abgelaufenes Passwort hat, dann …“ – es erhöht meiner Meinung nach die Lesbarkeit ernorm
  • Wenn ich „Randfälle“ (Exceptional Cases) behandeln will (siehe Defensive Programming) – dann verwende ich Exceptions

Und wann fange ich Exceptions?

However, the problem with catching System.Exception and allowing the application to continue running is that state may be corrupted [CLR via C#]

Wenn ich sicher gehen kann, …

  • … dass ich keinen Bug “hide”
  • … dass ich den State in einen konsistenten Zustand bringen kann
  • … dass ich den Benutzer meiner API nicht zwinge die Exception zu fangen (siehe Diskussion Java) „Flow Control Smell“
  • … dass es nicht anders geht (siehe Exogenous Exceptions)

Kleine Hilfsmittel

Polly vNext

“Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.”

Inzwischen habe ich schon 100 Mal Retry Methoden usw. gesehen – jeder macht es selber – weil man will sich im Regelfall nicht viele Abhängigkeiten ins Projekt holen. Doch Polly vNext sollte man sich wirklich mal anschauen, weil es eine grenzgeniale – und vor allem schlanke – Library ist. Neben Exception Retry sachen gibt es auch noch Sachen wie Circuit-breaker und Bulkhead Isolation.

https://github.com/App-vNext/Polly

https://gist.github.com/mvodep/b3d8946bbd369b4a2c1dc2c5619fcb8b

Code Conracts

Es gibt zwei Sachen, die mir den letzten Nerv rauben:

  • NullReferenceExceptions in langen Methoden
  • NullReferenceExceptions oder einfach falsche, unerwartete Werte die am anderen Ende des Codes eine Exception auslösen

Ein Hilfemittel: Preconditions – Postcondition – Invariant Bis Visual Studio 2015 muss man es nachinstallieren – ab 2017 inkludiert: https://marketplace.visualstudio.com/items?itemName=RiSEResearchinSoftwareEngineering.CodeContractsforNET. Eine sehr gute Einführung gibt’s hier http://www.albahari.com/nutshell/CodeContracts.pdf

Michael Vodep

Die Precondition gibt an, was beim Methodeneintritt zutreffen muss.

static string ToProperCase (string s)
{
 Contract.Requires (!string.IsNullOrEmpty(s));
 ...
}

Die Postcondition gibt an, was beim Methodenaustritt vorliegen muss.

static bool AddIfNotPresent<T> (IList<T> list, T item)
{
  Contract.Requires (list != null); // Precondition
  Contract.Ensures (list.Contains (item)); // Postcondition
 
  if (list.Contains(item))
  {
    return false;
  }
  
  list.Add(item);
 
  return true;
} 

Und die Invariante gibt an, was immer zutreffen muss.

class Test
{
  int _x, _y;
  
  [ContractInvariantMethod]
  void ObjectInvariant()
  {
    Contract.Invariant (_x >= 0);
    Contract.Invariant (_y >= _x);
  }
  
  public int X { get { return _x; } set { _x = value; } }
  public void Test1() { _x = -3; }
  
  void Test2() { _x = -3; }
} 

Code Contracts haben viele, viele Hilfsmethoden. Am Ende des Tages wird im Kompilierprozess noch was geändert (schaut euch den Code mit einem Reflektor an). Das schöne ist: es erspart viel, viel Kopfweh.

Aber war das schon alles? Nein! Der Code wird zusätzlich quasi formal verifiziert.

static void Main()
{
  string message = null;
  WriteLine (message); // Static checking tool will generate warning
}

static void WriteLine (string s)
{
  Contract.Requires (s != null);
  Console.WriteLine (s);
} 

Das heißt folgendes Beispiel würde einen Fehler ausgeben – zur Kompilierzeit.

Maintaining State (CLR via C#)

Man sollte – wenn möglich – gleich in der Methode seinen Mist wieder aufräumen. Dazu ein Beispiel:

public void SerializeObjectGraph(FileStream fs, IFormatter formatter, Object rootObj) {
  // Save the current position of the file.
  Int64 beforeSerialization = fs.Position;
  try {
    // Attempt to serialize the object graph to the file.
    formatter.Serialize(fs, rootObj);
  }
  catch { // Catch any and all exceptions.
    // If ANYTHING goes wrong, reset the file back to a good state.
    fs.Position = beforeSerialization;
    // Truncate the file.
    fs.SetLength(fs.Position);
    // NOTE: The preceding code isn't in a finally block because
    // the stream should be reset only when serialization fails.
    // Let the caller(s) know what happened by re-throwing the SAME exception.
    throw;
  }
}

Hier wird ein Stream serialisiert und es geht was schief. Dabei wird die alte Descriptor Position wiederhergestellt und die Exception weiter geworfen.

Hiding an Implementation Detail to Maintain a “Contract” [CLR via C#]

Meiner Meinung nach das wichtigste Pattern überhaupt – es wird so oft falsch gemacht.

internal sealed class PhoneBook {
  private String m_pathname; // path name of file containing the address book
  
  // Other methods go here.
  public String GetPhoneNumber(String name) {
    String phone;
    FileStream fs = null;
    try {
      fs = new FileStream(m_pathname, FileMode.Open);
      // Code to read from fs until name is found goes here
      phone = /* the phone # found */
    }
    catch (FileNotFoundException e) {
      // Throw a different exception containing the name, and
      // set the originating exception as the inner exception.
      throw new NameNotFoundException(name, e);
    }
    catch (IOException e) {
      // Throw a different exception containing the name, and
      // set the originating exception as the inner exception.
      throw new NameNotFoundException(name, e);
    }
    finally {
      if (fs != null) {
        fs.Close();
      }
    }
    
   return phone;
  }
}

Der Aufrufer von GetPhoneNumber müsste sich – wenn man es falsch macht – mit FileNotFoundException und IOExceptions herumschlagen. Schön und gut. Jetzt entscheidet der Entwickler der PhoneBook Library, dass er auf SQL Server umstellt, weil Files zu langsam sind. Es wird nun eine SQLException geworfen – damit rechnet der Aufrufer aber nicht. D.h., ich habe meine Implementierungsdetails nach außen weitergegeben – ein absolutes No-Go. Daher wurde in diesem Beispiel korrekterweise eine Exception NameNotFoundException eingeführt.

Robust Pattern

Wie können wir den State in C# am besten wiederherstellen? Mit sogenannten Constrained Execution Regions (CERs), welche in .NET mit AppDomains umgesetzt werden. Eine AppDomain kann ich „unloaded“ – und somit den ganzen State. Ich muss hier nicht teuer den ganzen Prozess abschießen, sondern kann einen Teil davon restarten.

Actor-Supervisor

Execution und Fehlerbehandlung sollten meiner Meinung nach immer getrennt werden. Eine Super-Visor Hierachie ist eine schöne Methode dazu, da sie erlaubt, eine hierarchische Fault-Tolerant-Style Applikation zu designen. Lest euch in das Thema ein – es ist genial: https://doc.akka.io/docs/akka/current/general/supervision.html. Vielleicht schaffe ich mal einen ganzen Blogeintrag dazu.

[1] https://en.wikipedia.org/wiki/Exception_handling#In_software
[2] https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/
[3] https://en.wikipedia.org/wiki/Exception_handling#In_software
[4] Bloch, J. (2008). Effective java (the java series). Prentice Hall PTR.
[5] http://staffwww.dcs.shef.ac.uk/people/A.Simons/remodel/papers/ExceptionsInEiffelAndJava.pdf
[6] http://wiki.c2.com/?DontUseExceptionsForFlowControl