Ausnahmebehandlung

in der Computertechnik ein Verfahren, Informationen über bestimmte Programmzustände an andere Programmebenen zur Weiterbehandlung weiterzureichen

Eine Ausnahme oder Ausnahmesituation (englisch exception oder Trap) bezeichnet in der Computertechnik ein Verfahren, Informationen über bestimmte Programmzustände – meistens Fehlerzustände – an andere Programmebenen zur Weiterbehandlung weiterzureichen.

Kann in einem Programm beispielsweise einer Speicheranforderung nicht stattgegeben werden, wird eine Speicheranforderungsausnahme ausgelöst. Ein Computerprogramm kann zur Behandlung dieses Problems dafür definierte Algorithmen abarbeiten, die den Fehler beheben oder anzeigen.

Exceptions haben in weiten Teilen die Behandlung von Fehlern mittels Fehlercodes oder Sprunganweisungen abgelöst und stellen im technischen Sinne einen zweiten, optionalen Rückgabewert einer Methode / Funktion dar.

Strukturierte Ausnahmebehandlung

Bearbeiten

Die größte Rolle in der Programmierung spielt dabei die strukturierte Ausnahmebehandlung (englisch structured exception handling, kurz SEH), eine Technik, die den Code zur Ausnahmebehandlung vom normalen Anwendungscode getrennt hält. Anstatt beispielsweise bei jedem Funktionsaufruf einen Rückgabewert, der den Erfolg anzeigt, zu prüfen und darauf zu reagieren, kann man die betreffende Funktion in Ausnahmesituationen eine Ausnahme auslösen lassen, die alle für die Problemerkennung und -behandlung erforderlichen Informationen in sich trägt.

Da die verursachende Funktion (oder die Funktion, die das Problem feststellt) in ihrem Kontext den Fehler möglicherweise nicht angemessen behandeln kann, wird die Exception so lange an aufrufende Funktionen zurückgereicht, bis schließlich eine die Exception „fängt“. Sinnvollerweise wird die Behandlung der Exception an einer Stelle im Code vorgenommen, in der die Konsequenzen dieser Ausnahme abzusehen sind, und eine entsprechende Reaktion sinnvoll zu implementieren ist (zum Beispiel Wiederholen der Benutzereingabe, Programmabbruch). Darüber hinaus sind Codeabschnitte zwischen Auftreten und Behandlung der Ausnahme im besten Falle frei von Fehlerbehandlungsroutinen.

Ein weiterer Vorteil der Ausnahmen gegenüber herkömmlichen Fehlercodes besteht darin, dass eine Exception nicht ignoriert werden kann. Ein Programmierer könnte vergessen, einen Rückgabewert zu prüfen, eine Exception jedoch wird immer weiter zurückgereicht – im Extremfall so lange, bis sie in der programmstartenden Funktion ankommt und somit unter Umständen den Programmfluss radikal unterbricht.

Fast alle neueren Programmiersprachen unterstützen die Ausnahmebehandlung, zum Beispiel Java, C++, C#, Python und Visual Basic .NET.

Verschiedene Hardware-Architekturen (wie zum Beispiel die IA-32-Architektur von Intel) unterstützen eine Exception-Behandlung auf Hardware-Ebene durch das Betriebssystem. Hierbei werden bei bestimmten ungültigen Operationen Software-Interrupts ausgelöst, die einen Einsprung in den privilegierten Betriebssystemkern verursachen. Dieser kann dann anhand der Exception das Programm mit einer Fehlermeldung beenden oder den Fehler an einen Debugger weiterleiten.

Checked Exceptions

Bearbeiten

In der Programmiersprache Java gibt es als Weiterentwicklung der Ausnahme die „Checked Exception“ (dt. etwa: überprüfte Ausnahme). Das ist eine Ausnahme, bei der der Compiler prüft, ob alle Stellen, wo sie auftreten kann, durch Code zum Abfangen der Ausnahme abgedeckt sind. Der Code zum Abfangen kann dabei innerhalb derselben Methode stehen, in der die Ausnahme auftreten kann, oder auch in aufrufenden Methoden. In letzterem Fall muss der Programmierer die Ausnahmen in der Methodensignatur deklarieren.

Die zugrunde liegende Idee beim Entwurf von Java war, dass Ausnahmen, auf die der Anwendungscode sinnvoll reagieren kann, als Checked Exception ausgeführt werden. Durch den Zwang zur Behandlung der Ausnahme sollte robuster Code erreicht werden und fehlende Fehlerbehandlungen bereits vom Compiler entdeckt werden.[1] Es gibt aber weiterhin Ausnahmen, die keine Checked Exceptions sind. Als Konvention gilt dabei, solche Fehler als Checked Exception zu realisieren, bei denen man vom Aufrufer erwartet, dass er auf ihn reagieren und einen geregelten Programmablauf wiederherstellen kann. Darunter fallen beispielsweise Netzwerk-, Datenbank- oder sonstige E/A-Fehler. So kann das Öffnen einer Datei aus verschiedenen Gründen fehlschlagen (keine Rechte, Datei nicht vorhanden), der Aufbau einer Netzwerkverbindung kann aus vom Programm nicht zu beeinflussenden Gründen fehlschlagen. Nicht-Checked-Exceptions sind zum Melden verschiedener Arten von Programmfehlern vorgesehen (zum Beispiel Indexfehler bei Array-Indizierung). Es wird davon abgeraten, die Anwendung in solchen Fällen versuchen zu lassen, einen geregelten Programmablauf wiederherzustellen.[2] Die Klassen der Java-Plattform selber halten sich weitgehend an diese Konvention.

Kritiker führen gegen die Checked Exceptions an, dass sie die Lesbarkeit des Quellcodes verschlechtern würden und dass sie viele Programmierer, weil sie in dieser Funktionalität keinen dem Aufwand entsprechenden Nutzen erkennen, zu Ausweichkonstrukten verleiten, die dem Compiler genügen, aber kaum Fehler behandeln.[3] Ein anderer Einwand ist, dass aufgrund der Deklaration der Exceptions in den Methodensignaturen allgemein verwendbare Hilfsklassen oder Interfaces, insbesondere als Teil von Entwurfsmustern, oft nicht sinnvoll operabel sind mit Klassen, die Checked Exceptions verwenden.[4] Als Ausweichlösung werden getunnelte Checked Exceptions vorgeschlagen, die aber den Nutzen der Checked Exception aufheben.[5] Darüber hinaus stehen Checked Exceptions als Teil der Methodensignatur der Erweiterbarkeit von Schnittstellen im Wege.

Neuere Fachliteratur sowie Diskussionen während der Entstehung von Programmiersprachen neueren Datums tendieren dazu, Checked Exceptions abzulehnen.[6][7]

Auslösen von Exceptions

Bearbeiten

Eine Exception kann an jeder Stelle im Programmcode ausgelöst werden. Dabei wird fast immer ein Objekt einer Exception-Klasse erzeugt und mit dem Schlüsselwort throw oder raise abgeschickt. Bei manchen Programmiersprachen (zum Beispiel C++) darf statt der Exception-Klasse auch jeder andere Datentyp verwendet werden.

Abfangen von Exceptions

Bearbeiten

Wird eine Exception im Programmablauf nicht explizit abgefangen, dann wird sie von der Laufzeitumgebung aufgefangen. Die Exception wird als Fehlermeldung angezeigt; je nach Art der Exception wird die Anwendung abgebrochen oder fortgesetzt. Eine Ausnahme zu behandeln oder nicht ist eine bewusste Designentscheidung, und je nach Art der Ausnahme sollte man abfangen oder nicht:[8]

  • Fatale Ausnahmen sind nicht dein Fehler, und sollten nicht abgefangen werden. Wenn zum Beispiel der Hauptspeicher des Computers ausgeht, ist nichts mehr zu machen, ab und zu macht es noch Sinn, einen final Block laufen zu lassen.
  • Dumme Ausnahmen, im Englischen „boneheaded Exceptions“, sind Programmierfehler. Es sollte immer so programmiert werden, dass kein Ausnahmezustand eintritt.
  • Lästige Ausnahmen, im Englischen „Vexing Exceptions“, sind Designfehler in anderer Software. Man fängt sie immer sofort. Ein bekanntes Beispiel ist das Parsen einer vom Benutzer eingegebene Zahl. Wenn sich die Person vertippt, kann ein Fehler auftreten, das weiß man schon vorher, ist ganz normal, und daher keine Ausnahme. In der Programmiersprache C# gab es ursprünglich nur Int32.Parse(), das eine Exception auslöst, später hat man noch Int32.TryParse() angeboten, wo der Fehler mit einem Rückgabewert behandelt werden kann.
  • Äußere Ausnahmen passieren selten, kann man gut behandeln, jedoch kaum vermeiden. Wenn man eine Datei öffnet, hat es keinen Sinn, vorher zu prüfen, ob sie existiert, da jemand die Datei bereits genau nach dieser Prüfung löschen kann. Solche Ausnahmen sollten immer behandelt werden.

Häufige Fehler bei der Ausnahmebehandlung sind:

  • Exceptions werden ohne weitere Aktionen geschluckt. Somit gehen alle Informationen über die eigentliche Fehlerursache verloren.
  • Exceptions werden durch eine eigene (häufig unzutreffende) Meldung ersetzt.

Es ist sinnvoll, Exceptions abzufangen, um zusätzliche Informationen anzureichern und erneut auszulösen.

Beispiele

Bearbeiten
ObjektA := TObjectA.Create;
try
  try
    BerechneEinkommen(Name);
  except
    on E:Exception do
    begin
      // Exception wurde abgefangen und wird um einen aussagekräftigen Hinweis ergänzt
      E.Message := 'Fehler beim Berechnen des Einkommens von ' + Name + #13#10 +
        E.Message; // ursprüngliche Meldung anhängen
      raise; // veränderte Exception erneut auslösen
    end;
  end;
finally
  FreeAndNil(ObjektA); // dies wird auf jeden Fall ausgeführt
end;
try {
    function1();
    function2();
   ...
} catch (const invalid_argument &e) {
    cerr << "Falsches Argument: " << e.what() << endl;
} catch (const range_error &e) {
    cerr << "Ungültiger Bereich: " << e.what() << endl;
} catch (...) {
    cerr << "Sonstige Exception" << endl;
}
MessageServiceClient client = new MessageServiceClient("httpEndpoint");
Message message = ... // Message erstellen
try
{
   client.AddMessage(message);
}
catch (FaultException fe)
{
   Console.WriteLine(fe.Message);
   client.Abort();
}
catch (CommunicationException ce)
{
   Console.WriteLine(ce.Message);
   client.Abort();
}
catch (TimeoutException)
{
   client.Abort();
}
MODULE BeispielAusnahmebehandlung;
  (* Es gibt vordefinierte und benutzerdefinierte Ausnahmen *)
FROM M2EXCEPTION IMPORT M2Exception, IsM2Exception, M2Exceptions;
  (* Deklarationen etc. *)
  IF SpeicherFehler THEN
    RAISE BenutzerdefinierteAusnahme;
  END;
BEGIN
  (* Hauptprogramm *)
EXCEPT
  (* Ausnahmebehandlung *)
FINALLY
  (* Finalisierung *)
EXCEPT
  (* Sollte bei der Finalisierung etwas schiefgehen *)
END BeispielAusnahmebehandlung.
' Versuche ...
Try
  ' ... die Methode oder Prozedur ...
  BerechneEinkommen(name)
' bei Ausnahme
Catch ex AS Exception
  ' gib Ausnahme aus
  MessageBox.Show("Fehler -> " & ex.message)
' Führe auf jeden Fall aus
Finally
  MessageBox.Show("Das wird trotzdem ausgeführt")
End Try
' Gibt den Inhalt einer Datei aus

Sub PrintFile(FileName As String)

  Dim hFile As File
  Dim sLig As String

  hFile = Open FileName For Read

  While Not Eof(hFile)
    Line Input #hFile, sLig
    Print sLig
  Wend

Finally ' Wird immer ausgeführt, sogar, wenn ein Fehler aufgetreten ist. Achtung: FINALLY muss vor CATCH kommen!
  Close #hFile

Catch ' Wir nur bei einem Fehler ausgeführt
  Print "Cannot print file "; FileName

End
try {
    // Fehlerträchtige Funktion ausführen
    uncertain_code();
} catch (OutOfMemoryError e) {
    // Ein Error ist keine Exception und muss separat abgefangen werden
    e.printStackTrace();
} catch (RuntimeException e) {
    // z. B. IndexOutOfBoundsException, NullPointerException usw.
    System.err.println("Offensichtlich ein Programmierfehler!");
    throw e; // Leite nach oben weiter
} catch (Exception e) {
    // Fange alle restlichen Ausnahmefehler ab
    e.printStackTrace();
} catch (Throwable t) {
    // Das hier fängt wirklich alles ab
    t.printStackTrace();
} finally {
    // Ob Exception oder nicht, führe das hier auf jeden Fall aus
    System.out.println("Berechnung beendet oder abgebrochen");
}
try {
    // Berechne ...
    rechnen();
    // Fehler generieren
    throw new Error("Fehler, Datei kann nicht gelesen werden");
} catch (e) {
    // Fehlertypen unterscheiden (Achtung: dies hier ist nur eine kleine Auswahl)
    if (e instanceof ReferenceError) {
        console.log(e.message);

    } else if(e instanceof SyntaxError) {
      console.log(e.message);

    } else {
      console.log(e);
    }
}
finally {
    // Wird immer ausgeführt, nachdem der Codeblock in "try" abgearbeitet ist
    weiter();
}
// Exceptionhandling ab PHP Version 5!
try {

    // Berechne ...

    // Fehlermeldung, Fehlercode
    throw new RuntimeException('Fehlermeldung', 543);

} catch (RuntimeException $e) {

    // z. B. IndexOutOfBoundsException, NullPointerException usw.
    // Wichtig: Um die hier verwendeten Basistypen zu nutzen,
    // muss die "SPL" installiert sein

    echo $e->getMessage();

    throw $e; // Leite nach oben weiter

} catch (Exception $e) {

    // Fange alle restlichen Ausnahmefehler ab und
    // verwende die __toString() - Methode zur Ausgabe

    echo $e;

} finally {

    // ab PHP 5.5
    // Code, welcher immer ausgeführt wird, auch
    // wenn ein Ausnahmefehler aufgetreten ist

}
try:
    result = do_something()
    if result < 0:
        raise ValueError

except ValueError:
    print('catching exception')
    do_something_else()

except Exception as e:
    # Catch the exception in "e" and print it.
    print('exception "{}" in method do_something'.format(e))

else:
    print('method do_something passed')

finally:
    print('always print this finally')

Siehe auch Python, Abschnitt Ausnahmebehandlung.

klassisch:

eval {
    something_fatal(...);
    1;
} or do {
    warn "Exception: $@";
};

ab Perl 5.34.0:

use feature 'try';
try {
    my $result = something_fatal(...);            # Ausnahme möglich
    defined($result) or die "something_fatal\n";  # hier auch
    send($result);
}
catch ($e) {         # Name beliebig, „my“ implizit
    warn "Exception: $e";
}
# ab Perl 5.36:
finally {
    clean_up_anyway();
}

Ab Version 2 unterstützt die Programmiersprache Swift von Apple auch Exceptions[9]:

enum MeinFehlerEnum: Error {
  case fehlerA
  case fehlerB
}
enum NochEinFehlerEnum: Error {
  case fehlerC
  case fehlerD
}

// Funktion kann Fehler mit "throw" auslösen, ist deshalb
// nach Parameter-Liste mit "throws" gekennzeichnet.
func ersteBerechnung(zahl1:Int, zahl2:Int) throws -> Int {
  if zahl1 == zahl2 { throw MeinFehlerEnum.fehlerA }
  return zahl1 + zahl2
}

// Funktion, die keinen Fehler auslösen kann.
func zweiteBerechnung(zahl1:Int, zahl2:Int) -> Int {
  return zahl1 * zahl2
}

do {

    // Die Funktion "ersteBerechnung()" ist eine throwing Funktion,
    // sie muss also mit einem vorangestellten "try" aufgerufen werden.
    let ergebnisEins = try ersteBerechnung(zahl1:3, zahl2:4)

    // Die Funktion "zweiteBerechnung()" ist KEINE throwing Funktion,
    // für ihren Aufruf ist also kein "try" erforderlich.
    let ergebnisZwei = zweiteBerechnung(zahl1:ergebnisEins, zahl2:42)

    print("Berechnungs-Ergebnis: \(ergebnisZwei)")

}
catch MeinFehlerEnum.fehlerA {
    print("Fehler A ist aufgetreten")
}
catch MeinFehlerEnum.fehlerB {
    print("Fehler B ist aufgetreten")
}
catch NochEinFehlerEnum.fehlerC {
    print("Fehler C ist aufgetreten")
}
catch { // Default-Catch
    print("Unerwarteter Fehler aufgetreten: \(error)")
}

Mit Swift 2.0 wurde ebenfalls das Schlüsselwort defer eingeführt, mit dem ein Block mit Code definiert werden kann, der auf jeden Fall beim Verlassen des aktuellen Blocks (z. B. Methode) ausgeführt wird, auch wenn dieser Block wegen einer Exception verlassen wird. Ein defer-Block kann also in etwa wie der aus anderen Programmiersprache bekannte finally-Block verwendet werden.[10]

Bearbeiten
  • Brian Goetz: Exceptional practices. In: JavaWorld. 3. August 2001, archiviert vom Original (nicht mehr online verfügbar) am 5. August 2001; (englisch, Artikelserie).
  • Kyle Loudon: Programming with Exceptions in C++. In: O’Reilly Network. 5. Mai 2003, archiviert vom Original (nicht mehr online verfügbar) am 10. Mai 2003; (englisch).
  • Karl Seguin: Understanding and Using Exceptions. In: CodeBetter.Com. 5. April 2006, archiviert vom Original (nicht mehr online verfügbar) am 8. April 2006; (englisch, .NET Framework).
  • Errors and Exceptions. In: The Python Tutorial. (englisch).

Einzelnachweise

Bearbeiten
  1. Ann Wohlrath: Re: Toward a more “automatic” RMI = compatible with basic RMI phi loso phy. In: java.sun.com. Ehemals im Original (nicht mehr online verfügbar); abgerufen am 9. Oktober 2008 (englisch).@1@2Vorlage:Toter Link/archives.java.sun.com (Seite nicht mehr abrufbar. Suche in Webarchiven)
  2. Joshua Bloch: Effective Java. Programming Language Guide. 1. Auflage. Addison-Wesley, 2001, ISBN 0-201-31005-8, S. 172 ff. (englisch, 272 Seiten).
  3. Bruce Eckel: Does Java need Checked Exceptions? Archiviert vom Original (nicht mehr online verfügbar) am 5. April 2002; abgerufen am 9. Oktober 2008 (englisch).
  4. Rod Waldhoff: Java’s checked exceptions were a mistake. In: Rod Waldhoff’s Weblog. 1. April 2003, archiviert vom Original (nicht mehr online verfügbar) am 3. Oktober 2003; abgerufen am 9. Oktober 2008 (englisch).
  5. Nat Pryce: Exception Tunneling. In: c2.com. 6. Oktober 2014, abgerufen am 4. Oktober 2022 (englisch).
  6. Robert Cecil Martin: Clean Code: A Handbook of Agile Software Craftsmanship. 1. Auflage. Pearson Education, 2009, ISBN 978-0-13-235088-4, S. 106 (englisch, 464 S.).
  7. Jon Skeet: Why doesn’t C# have checked exceptions? In: blogs.msdn.microsoft.com. Microsoft, 12. März 2004, abgerufen am 15. März 2017 (englisch).
  8. ericlippert: Vexing exceptions. In: Fabulous adventures in coding. 10. September 2008, abgerufen am 27. Oktober 2023 (englisch).
  9. Michael Kofler: try/catch in Swift 2. In: kofler.info. 24. Juli 2015, abgerufen am 17. Dezember 2016.
  10. Doug Gregor et al.: Swift-Changelog. In: GitHub-Repository von Apple. Apple Inc., 18. November 2016, abgerufen am 19. Dezember 2016 (englisch): „New defer statement. This statement runs cleanup code when the scope is exited, which is particularly useful in conjunction with the new error handling model.“