Home » Tutorials » Programmierkonzepte » Fehlerbehandlung

Fehlerbehandlung

Verhalten bei Fehlern

Es ist unvermeidlich, dass Programmierer Fehler produzieren. Diese Fehler dann zu finden ist eine Sache der Erfahrung. Trotzdem gelingt es nicht immer, einen Fehler schnell zu lokalisieren und solch eine Fehlersuche kann sich auch manches Mal über Stunden hinziehen. Kein Grund zu verzweifeln: zumeist genügt es, eine Pause einzulegen, um mit klarem Kopf die Fehlersuche wieder aufzunehmen und die eigene Strategie in Frage zu stellen.

Hinweise und Warnungen

Bevor es mit den Fehlern losgeht, ein paar Worte zu einem, von Einsteigern oft unterschätzten Thema: Der Compiler gibt uns nicht nur Fehlermeldungen aus, er macht uns auch auf mögliche Fehlerquellen aufmerksam. Syntaktisch kann der Quelltext also in Ordnung sein, möglicherweise enthält er jedoch Fehler in der Programmlogik.
Unser Ziel muss es demnach sein, dass solche Meldungen gar nicht erst entstehen, damit potentielle Fehlerquellen von vornherein ausgeschlossen werden.
Betrachten wir die vom Compiler erzeugten Hinweise folgender Funktion:

function Maximum(AZahl1, AZahl2: Integer): Integer;
var
  i: Integer; //H2164
begin
  Result := 0; //H2077
  if AZahl1 > AZahl2 then
    Result := AZahl1
  else
    Result := AZahl2;
end;

[DCC Hinweis] HinweiseUndWarnungen.dpr(5): H2077 Auf 'Maximum' zugewiesener Wert wird niemals benutzt
[DCC Hinweis] HinweiseUndWarnungen.dpr(3): H2164 Variable 'i' wurde deklariert, aber in 'Maximum' nicht verwendet

Der untere Hinweis ist schnell abgehandelt, denn offensichtlich wurde hier nur vergessen, die Variablendeklaration von i zu entfernen. Ein Doppelklick auf diesen Hinweis im Meldungsfenster führt uns direkt zur Fundstelle im Quelltext, also zur Deklaration der überflüssigen Variable i in der angegebenen, auf die gesamte Datei bezogenen Zeile – hier und in der Folge allerdings angepasst auf den jeweiligen Textausschnitt. Markieren durch Einfachklick und Anfordern der Hilfe durch Drücken von F1 zeigen uns weitere Informationen zu diesem Hinweis, ebenfalls zu erreichen durch Aufrufen der Hilfe und suchen nach der Meldungsnummer H2164.
Auch die Hinweismeldung der oberen Zeile spricht bereits für sich. Result wird in allen Fällen ein neuer Wert zugewiesen und damit ist die Initialisierung in Zeile 5 überflüssig und kann ebenfalls entfernt werden. Wie die Meldung außerdem zeigt, können innerhalb von Funktionen der Funktionsname und Result gleichrangig verwendet werden. Aus Gründen der Einheitlich- und Übersichtlichkeit verwenden wir jedoch ausschließlich Result.Während Hinweise meist stilistischer Natur sind, deuten Warnungen auf mögliche Fehlerquellen hin.

function Potenz(ABasis: Integer; AExponent: Cardinal): Integer;
var
  i: Integer;
begin
  //Result := 1; //Fehlende Initialisierung
  for i := 1 to AExponent do
    Result := Result * ABasis; //Lesender Zugriff auf Result
end;

[DCC Warnung] HinweiseUndWarnungen.dpr(8): W1035 Rückgabewert der Funktion 'Potenz' könnte undefiniert sein
Abgesehen davon, dass man sich hier um die mögliche Größe des Ergebnisses wenig Gedanken macht, wird Result nicht mit dem richtigen Wert vorbelegt. Der lesende Zugriff darauf liefert also dort einen Zufallswert und stellt einen Sonderfall einer nicht initialisierten lokalen Variable dar. Wäre die Funktion eine Prozedur und Result eine lokale Variable darin, dann würde sich folgende Warnmeldung ergeben:


[DCC Warnung] HinweiseUndWarnungen.dpr(8): W1036 Variable 'Result' ist möglicherweise nicht initialisiert worden

Glücklicherweise werden wir vom Compiler auf solche Schnitzer aufmerksam gemacht. Nicht immer ist jedoch direkt klar, was an unserem Quelltext eine Warnung hervorrufen sollte:

function Signum(AZahl: Integer): Integer;
begin
  if AZahl  0 then
      Result := 1
    else
      if AZahl = 0 then //Stiftet Verwirrung
        Result := 0;
end;

[DCC Warnung] HinweiseUndWarnungen.dpr(11): W1035 Rückgabewert der Funktion 'Signum' könnte undefiniert sein
Offensichtlich ist hier jeder mögliche Fall für AZahl abgehandelt worden und Result wird auch das richtige Ergebnis zugewiesen. Der Compiler erkennt jedoch in der letzten If-Anweisung eine Bedingung, für die es dann natürlich auch eine Alternative innerhalb eines Else-Zweiges geben könnte. Abhilfe schafft hier das einfache Entfernen der überflüssigen Bedingung, denn das abschließende Else behandelt alle anderen Fälle.
Gerade fehlende oder falsche Else-Zweige innerhalb von If- und Case-Anweisungen führen immer wieder zu schwer lokalisierbaren Fehlern. Handelt es sich nämlich nicht gerade um einen Initialwert, dann werden wir auch nicht auf einen fehlenden oder semantisch falschen Wert in der Fallunterscheidung hingewiesen. Ist man sich hier unsicher, so könnte man einfach im abschließenden Else eine Exception erzeugen, die auf den unbehandelten Wert in der Fallunterscheidung hinweist.Häufig sieht man auch folgende Warnmeldung:

var
  sTemp: AnsiString; //sTemp: string;
begin
  sTemp := '123';
  WriteLn(StrToInt(sTemp)); //Implizite Typumwandlung

[DCC Warnung] HinweiseUndWarnungen.dpr(5): W1057 Implizite String-Umwandlung von 'AnsiString' zu 'string'
Seit Delphi 2009 verweist der Alias String nicht mehr auf einen AnsiString, sondern auf einen UnicodeString. Da die internen String-Funktionen natürlich weiterhin mit dem Alias arbeiten, wandelt Delphi den AnsiString in einen UnicodeString um, was, in diese Richtung umgewandelt, auch nicht weiter fehleranfällig ist. Trotzdem bereinigen wir den Quelltext, in dem wir mittels string(sTemp) den Parameter für StrToInt explizit umwandeln oder direkt sTemp als string deklarieren. Das nackte Ergebnis der beiden Möglichkeiten ist erstmal gleich gut – die Warnung verschwindet.
Typumwandlungen haben aber immer eine latente Fehleranfälligkeit, denn man muss die beiden Typen bewerten können um zu wissen, ob hier unter Umständen Datenverlust möglich ist. Außerdem sollte man, solange kein gegenteiliger Grund vorliegt, immer mit den generischen bzw. dynamischen Typen arbeiten, denn der Compiler ist darauf optimiert – ausgenommen z.B. bei programmexterner Kommunikation mit einer Datei. Hier sind fundamentale bzw. statische Typen mit ihrer konstanten Darstellungsbreite, über Delphi-Versionen hinweg, von Vorteil.
Wie auch in den nächsten Unterkapiteln, können wir hier nur einige wenige Meldungen exemplarisch zeigen. Aber allein das Lesen und Verstehen der Hinweise und Warnungen sollte uns, in Kombination mit dem entsprechenden Hilfetext, zur Lösung des Problems führen.

Fehler zum Zeitpunkt der Kompilierung

Ein Programmierer kann ohne Einsicht in die Hilfe nicht programmieren, denn niemand hat alle Deklarationen und deren Aufbau und Zusammenhänge im Kopf. Syntaxfehler, welche jetzt behandelt werden, widersprechen diesem Aufbau und damit einer erfolgreichen Überprüfung durch den Compiler. Da aber alle Sprachmerkmale von Delphi in der Hilfe beschrieben sind, ist das unser Ansatzpunkt beim Beheben von Fehlern zum Kompilierungszeitpunkt.
Der wohl häufigste Fehler, ist ein unbekannter und damit undeklarierter Bezeichner:

program Syntaxfehler;
{$APPTYPE CONSOLE}
uses
  SysUtils{, Math};

var
  Eingabe: Integer;
  Ausgabe: Integer; //Ausgabe: TValueSign;
begin
  try
    Write('Ganzzahl: ');
    ReedLn(Eingabe); //Tippfehler
    Ausgabe := Sign(Eingabe); //Unit Math nicht eingebunden
    WriteLn('Signum= ', Ausgabe);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

[DCC Fehler] Syntaxfehler.dpr(12): E2003 Undeklarierter Bezeichner: 'ReedLn'
[DCC Fehler] Syntaxfehler.dpr(13): E2003 Undeklarierter Bezeichner: 'Sign'

Alle Deklarationen – also Konstanten, Funktionen, Klassen etc. – werden in Units abgelegt. Mit Ausnahme der Unit System, welche automatisch eingebunden wird, müssen wir dem Compiler durch Einbinden der entsprechenden Unit diese Deklarationen zugänglich machen. Findet der Compiler dann einen Bezeichner nicht, so kann das nur drei Gründe haben:

  • Der Bezeichner existiert grundsätzlich nicht, wie z.B. bei einem Tippfehler,
  • die Sichtbarkeit in der Unit ist an dieser Stelle nicht vorhanden, so z.B. beim Vertauschen der Reihenfolge von definierenden Deklarationen bzw. einer fehlenden Forward-Deklarierung oder
  • die benötigte Unit ist nicht eingebunden.

Dass ReedLn nicht durch die Hilfe zu erfassen ist, liegt an einem simplen Tippfehler im Quelltext.
Ein weiterer Grund für das Nichtauffinden eines Hilfeeintrags könnte sein, dass es sich um Bestandteile der Windows-Anwendungs-Programmierschnittstelle handelt. Um die Dokumentation der WinAPI einzusehen, muss im Menüpunkt Extras/Optionen/Hilfe/Online des Hilfe-Fensters das Laden der Hilfeinhalte angepasst werden und eine Internetverbindung bestehen.
Da wir einen Hilfeeintrag zu Sign vorfinden, interessiert uns die dort angegebene und von uns bisher nicht berücksichtigte Unit Math. Zusätzlich sehen wir eine Beschreibung der Funktionalität und der Eigenschaften von Sign, sowie die überladenen Deklarationen der Funktion. Hier erkennen wir im Rückgabetyp TValueSign zunächst einen Widerspruch zu unserer Variablendeklaration von Ausgabe, welche vom Typ Integer ist. Der im Hilfeeintrag zu TValueSign ersichtliche Integer-Teilbereichstyp wird hier aber niemals Probleme machen, weil ein Integer einen Teilbereich seiner selbst natürlich vollständig aufnehmen kann. Wenn man allerdings bedenkt, dass alle Integer-Typen untereinander zuweisungskompatibel sind, so könnte man hier auch einen vorzeichenlosen Cardinal, als Typ der Rückgabe, fehlerfrei kompilieren lassen.
Das Überführen von Daten in einen anderen Datentyp sollte somit immer sorgfältig geprüft werden – ein erfolgreiches Kompilieren allein reicht nicht aus!
Vielen Typumwandlungen schiebt aber bereits der Compiler einen Riegel vor:

var
  iWert: Integer;
  fWert: Real;
begin
  iWert := 1;
  fWert := iWert; //Zuweisungskompatibel
  iWert := Integer(fWert); //Trunc(fWert);

[DCC Fehler] Syntaxfehler.dpr(7): E2089 Ungültige Typumwandlung
Während der Integer-Wert noch zuweisungskompatibel zum Real-Wert ist, muss man für den umgekehrten Weg schon die Hilfe der Funktion Trunc in Anspruch nehmen, denn Integer- und Real-Werte können generell nicht per Typumwandlung ineinander überführt werden.Andere häufig auftretende Fehlermeldungen, auf die man gerade als Einsteiger trifft:

program Syntaxfehler;
{$APPTYPE CONSOLE}
uses
  SysUtils;

var
  iZahl1, iZahl2: Integer;
begin
  try
    Randomize //Fehlendes Semikolon
    iZahl1 := Random(10);
    iZahl2 := Random(1,0); //2 Parameter 
    if iZahl1 >> iZahl2 then //Nur >
      WriteLn(iZahl1, '>', iZahl2); //Abschluss der Anweisung durch ;
    else
      WriteLn(iZahl2, '>=', iZahl1);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

[DCC Fehler] Syntaxfehler.dpr(11): E2066 Operator oder Semikolon fehlt
[DCC Fehler] Syntaxfehler.dpr(12): E2034 Zu viele Parameter
[DCC Fehler] Syntaxfehler.dpr(13): E2029 Ausdruck erwartet, aber '>' gefunden
[DCC Fehler] Syntaxfehler.dpr(15): E2153 ';' nicht erlaubt vor einem 'ELSE'

Aufgelistet sind hier ausnahmslos Tipp- und Flüchtigkeitsfehler, und die Fehlermeldungen sprechen alle für sich. Interessant dabei ist, dass die Meldungen nicht immer auf die entsprechende Zeile verweisen.
Delphi erlaubt es, dass Anweisungen über mehrere Zeilen hinweg geschrieben werden dürfen und erst ein Semikolon beendet eine solche Anweisung. Fehlt ein Semikolon, so geht der Compiler zunächst davon aus, in der folgenden Zeile einen Operator zum Verknüpfen der Anweisungen vorzufinden. Ist das nicht der Fall, so ergibt sich der Widerspruch, dann jedoch in dieser nachfolgenden Zeile.
Gerade Fehlermeldungen mit fehlenden Semikolons gibt es in einigen Ausprägungen und Fehler dieser Art können auch Folgefehler hervorrufen. Deswegen ist es angebracht, solche Fehlerlisten von oben herab abzuarbeiten und zwischendurch einfach mal neu zu kompilieren.

Interpretieren von Laufzeitfehlern

Laufzeitfehler können sich nicht nur für Einsteiger zu einem echten Problem entwickeln. Die Art und Weise wie sie entstehen sind vielfältig und für deren Lösung gibt es kein Patentrezept, denn neben Programmierfehlern können hier z.B. auch hardware-spezifische Faktoren eine Rolle spielen. Bei Zugriffsverletzungen kann es sich um Speicherbereich handeln, der schon sehr viel früher im Quelltext falsch angesprochen wurde, die Auswirkungen darauf aber erst später zum Tragen kommen und das möglicherweise bei verschiedenen Programmläufen an unterschiedlichen Stellen. Daher ist es wichtig, die Fehlermeldung richtig einzuordnen, insbesondere, um in einem eventuellen Debugging-Prozess die notwendigen Rückschlüsse ziehen zu können.
Betrachten wir folgenden Klassiker der Laufzeitfehler, eine Division durch 0:

program Laufzeitfehler1;
{$APPTYPE CONSOLE}

var
  i: Integer;
begin
  i := 0;
  WriteLn('1 geteilt durch 0 = ', 1 div i); //Division durch 0
  ReadLn;
end.

Speichern und erzeugen wir das Projekt und starten das Programm mit Unterstützung des Debuggers, so sehen wir nur ein kurzes Aufflackern der Konsole – das Programm terminiert direkt, trotz der abschließenden Aufforderung zum ReadLn. Was wirklich passiert ist, können wir hier nicht erkennen. Dazu starten wir eine separate Konsole und führen die erzeugte Datei Laufzeitfehler1.exe dort direkt und somit außerhalb von Delphi aus.
Durch Ausführen von cmd.exe im Windows-Startmenü, startet die Eingabeaufforderung im Verzeichnis des Benutzerprofils. Die ausführbaren Dateien werden standardmäßig im Unterordner Win32Debug des in der Umgebungsvariablen BDSPROJECTSDIR angegebenen Verzeichnisses gespeichert. Einzusehen ist dieser Wert im Hauptmenü unter Tools/Optionen/Umgebungsoptionen/Umgebungsvariablen. Wurde das Projekt nicht gezielt an einem anderen Ort gespeichert oder diese Variable geändert, dann wechseln wir nun, exemplarisch auf einem deutschen Windows 7, mit

cd "Eigene DateienRAD StudioProjekteWin32Win32"
ins entsprechende Verzeichnis und starten das Programm durch Eingabe von

Laufzeitfehler1.exe
Wir erhalten folgende Fehlermeldung:

bild8.png

Schließen wir den Windows-Fehlerdialog, dann erkennen wir an der Konsolenausgabe einen Laufzeitfehler 200 an einer bestimmten Adresse.

1 geteilt durch 0 = Runtime error 200 at 004050E1
Diese Fehlerausgabe wurde ebenfalls durch das Betriebssystem erzeugt und nicht etwa durch Delphi. Das ist auch nicht weiter verwunderlich, da wir im Quelltext überhaupt keine Fehlerbehandlung implementiert haben. Fehler werden solange in der Hierarchie nach oben weitergereicht, bis sich jemand dafür verantwortlich fühlt – eine entsprechende Fehlerbehandlung vorgesehen hat. In letzter Instanz also Windows selbst, das die Anwendung in einem geschützten Speicherbereich ausführt. Was genau die Ursache des Programmabsturzes war, bleibt uns bei einer solchen Fehlermeldung natürlich verborgen. Es muss jedoch unser Anliegen sein, solche Programmabstürze generell abzufangen.
Die Funktionalität, die dafür notwendig ist und die Laufzeitfehler in Exceptions umwandelt, liegt in der Unit Sysutils. Was bei einer VCL-Formularanwendung jedoch noch weitegehend in der Methode Application.Run vom Benutzer ferngehalten wird, ist in einer Konsolenanwendung direkt im Quelltext sichtbar: Das Umschließen des relevanten Quelltextes mit einer Try-Except-Anweisung. Im Gegensatz zur Formularanwendung ist der Programmlauf hier aber nicht ereignisgesteuert, sondern folgt linear dem Hauptprogramm. Daraus ergibt sich die Notwendigkeit, eben dieses Hauptprogramm vor Laufzeitfehlern, welche zu Exceptions bzw. Ausnahmen führen, zu schützen. Wir ergänzen also den Text von oben, mit dem von Delphi vorgesehenen Gerüst der Fehlerbehandlung:

program Laufzeitfehler;
{$APPTYPE CONSOLE}
uses
  SysUtils;

var
  i: Integer;
begin
  try
    i := 0;
    WriteLn('1 geteilt durch 0 = ', 1 div i); //Division durch 0
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  ReadLn;
end.

bild9.png

Diese Meldung stammt nun von der Delphi-Laufzeitumgebung, in der unser Programm ausgeführt wird. Der Unterschied zur Windows-Ausgabe ist deutlich, denn es wird uns hier ein klarer Grund für die Ausnahme genannt. Wir wissen also jetzt, was passiert ist, kennen aber noch nicht die Stelle des Quelltextes, an der die Ausnahme aufgetreten ist. Der Dialog bietet uns aber zwei erwähnenswerte Möglichkeiten: Anhalten und Fortsetzen.
Fortsetzen lässt das Programm einfach weiterlaufen. Es verzweigt darauf in die Ausnahmebehandlung und wartet dann auf das abschließende Return. Das Programm stürzt also nicht ab, der Fehler wurde abgefangen. Da Konsolenanwendungen jedoch selten interaktiv sind und üblicherweise über Programmparameter gesteuert, direkt aus der Konsole gestartet werden, ist ein abschließendes ReadLn eher unerwünscht und hier nur zum Offenhalten der Konsole eingebaut.

1 geteilt durch 0 = EDivByZero: Division durch Null
Diese Ausgabe schlussendlich stammt von unserem Programm und wäre auch das Einzige, das wir sehen würden, würden wir Laufzeitfehler.exe in einer separaten Konsole starten. Dadurch erkennen wir, dass die komplette Fehlerbehandlung, durch Einbinden der Unit Sysutils, im Programm vorhanden ist.
Die Meldung selbst listet uns die Fehlerklasse und die entsprechende Fehlermeldung. Was weiterhin fehlt ist der Ort des Fehlers.
Dazu wählen wir im Dialog nicht Fortsetzen, sondern Anhalten. Im Quelltext wird dadurch die Zeile markiert, in der die Ausnahme aufgetreten ist. Hier können wir jetzt die Variablen auswerten oder uns z.B. schrittweise durch den Quelltext weiterbewegen.
In der Praxis ist es aber meist sinnvoller, den Zustand aller relevanten Variablen direkt vor Auftreten des Fehlers zu kennen, sofern dieser konstant in einer Zeile auftritt. Man würde also einen Haltepunkt in einer Zeile darüber setzen und einen neuen Programmlauf damit dort stoppen, die Variablen auswerten und sich dann schrittweise der Problemstelle nähern.
In einer reelen Konsolenanwendung würde jede Ausnahme, die ausschließlich auf die gezeigte Art und Weise behandelt wird, schlussendlich zu einem kontrollierten Beenden des Programms führen. Das ist ein Unterschied zu einer VCL-Formularanwendung, die nach einer von uns unbehandelten Ausnahme ein Meldungsfenster anzeigt und danach weiterhin auf Benutzerinteraktion wartet.
Genauso wie in einer Formularanwendung würden wir aber auch hier einzelne Funktionen und Bereiche, die Ausnahmen erzeugen können, durch eine eigene Fehlerbehandlung schützen und somit einem vorzeitigen Programmende entgehen. Das soll aber nicht heißen, dass dann jede kritische Funktion in einen Try-Except-Block eingeschlossen wird, denn eine Exception-Behandlung kostet Zeit und Ressourcen. Außerdem erzeugen z.B. WinAPI-Funktionen überhaupt keine Exceptions, sondern liefern zumeist einen Rückgabewert, der ausgewertet werden muss.
Im gezeigten Beispiel der Division durch Null wurde der Fehler natürlich hinkonstruiert. Liegt die Division in einer etwas komplexeren Form vor, so würden wir den Divisor in einer eigenen Variablen berechnen und diese gegen 0 prüfen. Solch eine Überprüfung der Eingangsdaten ist ein probates Mittel zur Fehlerminimierung. Genauso sollten beim Austesten der Funktionalität gerade die Randstellen des Wertebereichs ausgiebig untersucht werden. Handelt es sich um Benutzereingaben, dann ist die Validierung der Eingangsdaten sogar unerlässlich, denn neben gezielten Falscheingaben müssen auch immer Tippfehler in die Überlegung mit einbezogen werden.Schauen wir uns eine solche Datenvalidierung am Beispiel einer Formularanwendung genauer an:

unit ULaufzeitfehler2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  dividend, divisor, division: Integer;
begin
  dividend := StrToInt(Edit1.Text);
  divisor := StrToInt(Edit2.Text);
  if divisor  0 then
  begin
    division := dividend div divisor;
    ShowMessage(IntToStr(division));
  end
  else
    ShowMessage('Division durch 0');
end;

end.

Auf dem Formular befinden sich zwei TEdit und ein TButton, deren Namen nicht verändert wurden. Warum eine sinnvolle Namensgebung aber durchaus wichtig ist, wird in einem späteren Abschnitt noch gezeigt. Die beiden TEdit nehmen Dividend und Divisor auf, und der TButton zeigt uns, ausgelöst durch die entsprechende Ereignisbehandlungsroutine, das Ergebnis bzw. den abgefangenen Fall einer Division durch 0. Die Benutzereingaben wurden dabei allerdings keiner gesonderten Überprüfung unterzogen und werden durch StrToInt genauso verarbeitet, wie sie in den TEdits stehen. StrToInt erwartet dabei einen String, den es in einen Integer umwandeln kann. Was aber passiert, wenn dieser String eben nicht in eine Ganzzahl umgewandelt werden kann? Wenn z.B. der String leer ist, ein Komma, einen Punkt oder generell irgendwelche Sonderzeichen enthält? Wir testen das durch die Eingabe von „a“. Die Laufzeitumgebung zeigt uns daraufhin folgende Fehlermeldung:

Im Projekt Laufzeitfehler2.exe ist eine Exception der Klasse EConvertError mit der Meldung '"a" ist kein gültiger Integer-Wert' aufgetreten.
Wie schon im Beispiel der Division durch 0 gibt uns die Fehlerklasse auch hier an, in welche Kategorie wir den Fehler einordnen können. Fehlerklassen gibt es viele und jede Fehlerklasse ist eine grobe Einteilung in die Art der Fehler. So können unterschiedliche, aber in ihrer Art gleiche Fehler ein und dieselbe Fehlerklasse hervorrufen.
EConvertError, wie der Name schon sagt, behandelt dabei Konvertierungsfehler. Dazu zählen z.B. Umwandlungen von String oder zu String, aber auch fehlerhafte Zuweisungen typfremder Komponenten. Hier bietet uns die Hilfe zur entsprechenden Fehlerklasse immer einen grundsätzlichen Überblick an.
Die darauf folgende Fehlermeldung zeigt uns dann einen genaueren Aufschluss zu dem konkreten Fehler. Im Allgemeinen können wir mit diesen Informationen und dem Wissen um die Stelle des Fehlers das Problem genügend eingrenzen und bestenfalls direkt lösen.

"a" ist kein gültiger Integer-Wert
Diese vom Programm erzeugte Fehlermeldung ist aus Benutzersicht die einzige Information, die er vom fertigen Programm erhält, und obwohl der Fehler nicht speziell behandelt wurde ist das Programm weiterhin bedienbar. Trotzdem gilt es natürlich auch hier diesen Fehler abzufangen und gar nicht erst entstehen zu lassen.
Delphi bietet dazu eine ganze Reihe von Funktionen an, die Konvertierungen zwischen Strings auf der einen Seite und Ganzzahlen, reellen Zahlen, Datumswerten und ähnlichem mehr auf der anderen Seite ermöglichen.
Diese Funktionen mit dem Aufbau TryStrToXXX und TryXXXToStr geben den Erfolg der Konvertierung als Boolean-Wert zurück. Eine weitere Möglichkeit Strings in einen bestimmten Datentyp zu konvertieren sind Funktionen des Aufbaus StrToXXXDef, die bei Misserfolg einen übergebenen Default-Wert zurückgeben.

procedure TForm1.Button1Click(Sender: TObject);
var
  dividend, divisor, division: Integer;
begin
  if TryStrToInt(Edit1.Text, dividend) and TryStrToInt(Edit2.Text, divisor) then
  begin
    if divisor  0 then
    begin
      division := dividend div divisor;
      ShowMessage(IntToStr(division));
    end
    else
      ShowMessage('Division durch 0');
  end
  else
    ShowMessage('Ungültige Ganzzahl-Eingabe');
end;

Kann einer der beiden Strings nicht erfolgreich umgewandelt werden, so wird die Berechnung nicht gestartet und für den Benutzer erscheint eine aussagekräftige Mitteilung. Im Zuge einer guten Benutzerführung könnte man auch die beiden Edits einzeln auswerten und bei Misserfolg zusätzlich den Fokus dem entsprechenden Edit zuweisen.
Bisher hatten wir es immer mit sehr aussagekräftigen Fehlermeldungen zu tun, die uns schnell der Lösung des Problems näher brachten. Bei AccessViolations bzw. Zugriffsverletzungen ist das nicht der Fall. Zugriffsverletzungen entstehen dann, wenn auf Ressourcen zugegriffen wird, die nicht erreichbar oder geschützt sind. In der Praxis bedeutet das, dass wir lesend oder schreibend auf Speicher zugreifen, der dafür nicht vorgesehen ist. In den meisten Fällen werden dabei Zeiger dereferenziert, die nil (not in list) oder ungültig sind:

procedure ErzeugeZugriffsverletzung;
var
  sListe: TStringList;
begin
  //sListe := nil;
  sListe.Create;

Das Create wird hier nicht an der Klasse aufgerufen sondern fälschlicherweise direkt an der Instanz. Eine Instanz hat aber nur dann eine gültige Adresse, wenn sie erzeugt wurde. Damit misslingt die Dereferenzierung und führt zu folgender Zugriffsverletzung:

Im Projekt Zugriffsverletzung1.exe ist eine Exception der Klasse EAccessViolation mit der Meldung 'Zugriffsverletzung bei Adresse 0043B5FC in Modul 'Zugriffsverletzung1.exe'. Schreiben von Adresse 00421396' aufgetreten.
Die angegebenen hexadezimalen Adressen, welche beim Leser variieren werden, sind hilfreicher als man das im ersten Augenblick vermuten könnte. Die erste Adresse ($0043B5FC) beschreibt den Ort des Fehlers, dort ist die Zugriffsverletzung aufgetreten. Praktischen Nutzen hat diese Adresse als Adresshaltepunkt beim Debugging.
Rückschlüsse zur zweiten Adresse können wir nur ziehen, wenn die Adresse nahe bei 0 liegt. Um das zu verdeutlichen entkommentieren wir im Quelltext die nil-Zuweisung und schauen uns die Fehlermeldung erneut an.

Im Projekt Zugriffsverletzung1.exe ist eine Exception der Klasse EAccessViolation mit der Meldung 'Zugriffsverletzung bei Adresse 0043B5FC in Modul 'Zugriffsverletzung1.exe'. Schreiben von Adresse 0000000C' aufgetreten.
Der Ort des Fehlers ist derselbe. An der 2. Adresse ist jedoch eine Veränderung zu sehen und diese wird als 0+Offset interpretiert. 0 bedeutet, dass der Zeiger nil ist. Das Offset von 12 Bytes (hexadezimal = C) für Create muss uns nicht weiter interessieren, es ist unterschiedlich bei verschiedenen Klassen bzw. entfällt ganz. Relevant ist der nil-Zeiger, denn auf nil können wir prüfen bevor wir auf eine Instanz zugreifen. Das macht man sich zunutze, wenn häufiger Instanzen erstellt und wieder gelöscht werden sollen.

procedure TForm1.Button1Click(Sender: TObject);
//Stringliste erstellen
begin
  if not Assigned(sListe) then
    sListe := TStringList.Create;
end;

procedure TForm1.Button2Click(Sender: TObject);
//Stringliste löschen und nil setzen
begin
  if Assigned(sListe) then
    FreeAndNil(sListe);
end;

Wir erstellen die Liste nur dann, wenn sie nicht existiert und löschen sie nur dann, wenn sie existiert. Die Prüfung erfolgt durch Assigned, das intern auf nil prüft. Somit muss dieser Zustand nil explizit gesetzt werden, was FreeAndNil für uns erledigt. Ein einfaches Free würde die Instanz zwar auch freigeben, allerdings wäre der Zeiger darauf noch immer belegt und eine Prüfung würde uns eine vorhandene Instanz vortäuschen.
Probleme in der Analyse dürften eigentlich nur bei Logik- und Laufzeitfehlern entstehen. Ein allgemeingültiges Konzept zur Lösung kann allerdings nicht gezeigt werden, da die Herangehensweise von Fall zu Fall variiert. Es ist es aber grundsätzlich sinnvoll die Problemstelle einzukreisen und den Quelltext auf das Nötigste zu reduzieren.