Motivation: Warum Ausnahmen?

Motivation: Gängige Fehlersituationen

  • Datei soll geladen werden, existiert aber nicht
  • Netzwerkverbindung ist nicht verfügbar
  • String „a34b“ soll in einen Integer umgewandelt werden
  • Programm versucht 15 / 0 zu berechnen
  • Methode greift hinter das letzte Element eines Arrays
  • Es ist kein Speicher mehr auf dem Heap vorhanden
  • Ein Programm gerät in eine unendliche Rekursion und verbraucht den gesamten Stack-Speicher
  • Methode versucht, einen String auf einen Integer zu casten

Dass in Programmen Fehler auftreten, ist ganz normal. Häufig liegt die Ursache für ein Problem nicht im Programm selbst, sondern in der Umgebung oder den Eingaben des Benutzers. So gibt der/die Benutzer:in möglicherweise einen falschen Dateinamen an oder das Netzwerk funktioniert während eines entfernten Zugriffs nicht richtig.

Insofern muss sich eine gute Software immer um Fehlersituationen kümmern. Es ist sogar so, dass häufig noch ein erheblicher Aufwand zwischen einer Software, die die gewünschte Funktionalität ausführt und einer Software, die in Fehlersituationen richtig funktioniert, liegt.

Das Problem

Wie transportiert man die Information, dass ein Fehler aufgetreten ist dorthin wo er korrigiert werden kann?

  • Sofortiger Abbruch des Programms?
  • Setzen einer globale Fehlervariable?
  • Magischer Rückgabewert?
  • Fehler ignorieren und protokollieren?
  • Aufruf eines Callbacks zur Fehlerbehandlung?
  • Einfach Standardwerte annehmen und weitermachen?

Keine der hier aufgezeigten Methoden ist besonders befriedigend. Trotzdem kommen sie bei vielen Programmiersprachen zum Einsatz. So ist das sofortige Abbrechen des Programms der Standardfall in vielen Sprachen (auch in Java), wenn sich der/die Programmierer:in nicht um den Fehler kümmert.

Das Setzen einer Fehlervariable und magische Rückgabewerte kommen in C zum Einsatz. Visual Basic bis Version 6 bietet mit On Error Goto sowohl Callbacks als auch mit On Error Resume Next die Möglichkeit, Fehler einfach zu ignorieren und beim nächsten Statement weiterzuarbeiten. Es ist offensichtlich, dass das Ignorieren von Fehlern keine ernsthafte Fehlerbehandlung darstellt und Probleme oft noch verschlimmert.

Beispiel: Fehlerbehandlung in C

if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < O) {
     DieWithError("socket () failed") ;
}
/* Bind to the local address */
if (bind(servSock, (struct sockaddr *)&echoServAddr,
        sizeof(echoServAddr)) < O) {
    DieWithError ("bind () failed");
}
/* Mark the socket so it will listen for incoming connections */
if (listen(servSock, MAXPENDING) < O) {
    DieWithError("listen() failed") ;
}
for (;;) { /* Run forever */
    if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr,
            &clntLen)) < O) { /* Wait for a client to connect */
        DieWithError("accept() failed");
    }
}

Man sieht hier an dem C-Beispiel sehr deutlich die Probleme klassischer Fehlerbehandlung über Rückgabewerte:

  • Die normale Programmlogik und die Fehlerbehandlung sind wild gemischt.
  • Der Programmcode wird erheblich aufgebläht und sehr unübersichtlich, da manche IFs der Fehlerbehandlung dienen und andere wieder dem normalen Kontrollfluss.
  • Der Rückgabewert von Funktionen ist doppeldeutig: er transportiert sowohl Fehler- als auch normale Informationen.
  • Es kann leicht passieren, dass man vergisst einen Fehlercode abzufragen, sodass Fehler überhaupt nicht behandelt werden.
  • Es ist sehr schwer Fehler weiterzureichen, da die hier dargestellte Funktion vier verschiedene Fehlersituationen im eigenen Rückgabewert codieren müsste.

Anforderungen an Fehlerbehandlung

  • Fehlerbehandlung und normaler Betrieb sind orthogonal
  • Fehler können nicht ignoriert werden und werden garantiert behandelt
  • Fehlerbehandlung orientiert sich an der Aufrufhierarchie
  • In Aufrufhierarchie kann an beliebiger Stelle reagiert werden
  • Fehlerbehandlung soll objektorientiert sein
  • Eigene Fehler können definiert und verarbeitet werden
  • Fehlerbehandlung und normaler Betrieb sind orthogonal – Die Fehlerbehandlung und die normale Programmlogik sollen sich im Programm nicht vermischen, sondern es soll jederzeit klar erkennbar sein, bei welchen Statements es sich um Fehlerbehandlung handelt und bei welchen nicht. Weiterhin sollen normale Logik und Fehlerbehandlung an getrennten Stellen stehen und sich nicht Zeile für Zeile mischen.
  • Fehler können nicht ignoriert werden – Es soll den Programmierer:innen nicht möglich sein, Fehlersituationen einfach zu ignorieren und Fehler nicht abzufragen. Gerade Fehler, die nicht behandelt werden, führen häufig zu erheblichen Problemen, weil das Programm auf einem Bein weiter humpelt, um dann an einer ganz anderen Stelle (fern ab der eigentlichen Ursache) zusammenzubrechen. Hierdurch sucht man dann den Fehler an einer völlig falschen Stelle.
  • Fehlerbehandlung orientiert sich an der Aufrufhierarchie – Häufig liegt die Ursache für einen Fehler nicht an der Stelle, an der er auftritt. Gibt z. B. ein/eine Benutzer:in einen falschen Dateinamen an, so tritt der Fehler im Programm an der Stelle auf, an der die Datei geöffnet werden soll, die Ursache ist aber die falsche Eingabe. Daher möchte man Fehler die Aufrufhierarchie hinauf transportieren können, bis sie beim Verursacher ankommen.
  • In Aufrufhierarchie kann an beliebiger Stelle reagiert werden – Natürlich möchte man beim Transport entlang der Aufrufhierarchie an jeder beliebigen Stelle reagieren können.
  • Fehlerbehandlung soll objektorientiert sein – In einer objektorientierten Sprache wie Java wäre es armselig, wenn man nicht die Fehlerbehandlung nach objektorientierten Kriterien aufbauen würde. Andernfalls wäre die Fehlerbehandlung ein Fremdkörper in der Sprache.
  • Eigene Fehler können definiert und verarbeitet werden – Der/die Programmierer:in sollte in der Lage sein, eigene Fehlerarten entwerfen und behandeln zu können.

Copyright © 2025 Thomas Smits