Fehlerbehandlung

Letzte Änderung am 9. März 2022 by Christoph Jüngling

Ich glaube, es wird mal Zeit für dieses Thema, das vielleicht sowohl Profis als auch Anfänger interessieren könnte. Ich verwende das Prinzip jetzt schon viele Jahre, und es ist mir so selbstverständlich geworden, als ob es zu VBA dazugehören würde. Tut es im Grunde auch, denn alles, was ich hier mache, erfolgt mit Bordmitteln von VBA – kein einziges ActiveX, keine Lizenz, keine Updateprobleme. Und das war mir wichtig. Dennoch, vor den Erfolg haben die Götter das Denken gesetzt.

Ein Hinweis vorab: Siehe zu diesem Thema auch die Mini-Artikelserie über vbWatchDog. Das geht dann zwar nicht mehr ganz ohne Lizenz (und folglich auch nicht ohne Kosten), aber dafür hat man erheblich weniger Aufwand im Code.

Grundsätzliches

Worum geht es eigentlich? Der Hintergrund ist die saubere Fehlerbehandlung in VBA – oder VB6, falls da noch jemand mit arbeitet. Beide Sprachkonzepte sind weitgehend identisch, und die kleinen Unterschiede auch zwischen diesen und den einzelnen Office-Applikationen spielen für diese Betrachtung keine Rolle.

Wie es sich gehört, betrachten wir zunächst die Anforderungen an das Konzept:

  • In jeder Sub/Function muss mit wenig Aufwand eine Fehlerbehandlung einbaubar sein
  • Es müssen alle verfügbaren Informationen in die Fehlermeldung eingehen
  • Es muss eine einfache Möglichkeit geben, Instanzen kontrolliert zu schließen, bevor die Sub/Function beendet wird
  • Informationen aus früheren Fehlern müssen erhalten bleiben
  • Es darf keine externen Abhängigkeiten geben, d.h. alles soll aus VBA-Bordmitteln gebaut sein
  • Alles soll leicht in andere Projekte übertragbar sein
  • Der Benutzer soll eine MessageBox angezeigt bekommen, in der alle Informationen enthalten sind

Wir nehmen die subtilen Unterschiede zwischen “soll” und “muss” bzw. “darf nicht” wahr? Das ist Absicht.

Welche Informationen stehen zur Verfügung?

Nehmen wir den einfachsten Fall, führen bewusst fehlerhaften Code aus und schauen, was passiert. (Es ist übrigens egal, ob du das in Access, Excel oder Word machst.) Geh mit Alt+F11 in den VB-Editor, lege ein Modul an und schreibe oder kopiere den folgenden Code dort hinein:

Public Sub Test()

Debug.Print 1/0

End Sub

Klar, dass da ein Fehler auftauchen muss. Setze den Cursor in die Sub hinein und drücke F5. Es wird ein Dialog aufgemacht, in dem “Laufzeitfehler 11: Division durch Null” steht. Eigentlich ist das sogar falsch, denn “Null” ist in VBA ja etwas anders als “0”, aber das nur am Rande. Ein Klick auf “Debuggen” markiert erwartungsgemäß die Zeile mit dem Debug-Befehl gelb; das ist also die Zeile mit dem Fehler.

Das Laufzeitsystem “weiß” also mindestens folgendes:

  • Worin genau bestand das Problem (Nummer und Text)?
  • Wo (d.h. in welcher Codezeile) ist das Problem aufgetreten?

Auch wenn die Codezeile bekannt ist stellt sich die Frage, wie wir diese kommunizieren, wenn wir als Entwickler gerade nicht vor dem Bildschirm sitzen. Außerdem sollte es dem Anwender nicht zugemutet werden, im Code zu landen, mit allen Folgeproblemen wie “Ausführung stoppen”, “Fenster schließen”, “Programm beenden” etc.

Also nun ein zweiter Versuch, diesmal etwas aufwändiger. Wir fügen eine Fehlerbehandlung ein, wo einfach nur alle Informationen über den Fehler in das Direktfenster ausgegeben werden:

Public Sub Test()

On Error GoTo Catch

42 Debug.Print 1 / 0

Exit Sub

Catch:
Debug.Print Err.Number
Debug.Print Err.Description
Debug.Print Err.Source
Debug.Print Erl

End Sub

Nochmal mit F5 starten, bitte. Nun bekommen wir kein Fenster mehr, statt dessen steht im Direktfenster:

 11 
Division durch Null
VBAProject
 42

Wir haben es also wieder mit dem Fehler Nummer 11 zu tun, auch der beschreibende Text ist der gleiche wie vorhin, das VBAProject ist die Quelle des Fehlers, und er ist in Zeile 42 aufgetreten. “VBAProject” ist übrigens der Standardname des Projektes, den man aber über “Extras / Eigenschaften / Projektname” auch ändern kann. Nicht so wichtig im Moment.

Schön wäre es, wenn jetzt noch irgendwie der Name der Subroutine und des Moduls (oder der Klasse) hier stehen würde. Das geht bei Modulen so einfach leider nicht, da VBA dies meines Wissens nicht bereit hält. Aber dagegen können wir etwas tun. Diesmal zeige ich das gesamte Modul mit künstlichen Zeilennummern (die “42” ist die einzige, die in unserem Code vorkommt):

Option Explicit

Private Const CLASS_NAME = "Testmodul"

Public Sub Test()

Const FUNCTION_NAME = "Test"

'-----------------------

On Error GoTo Catch

42 Debug.Print 1 / 0

Exit Sub

Catch:
Debug.Print Err.Number
Debug.Print Err.Description
Debug.Print Err.Source + " / " + CLASS_NAME + "." + FUNCTION_NAME
Debug.Print Erl

End Sub

UPDATE: Ich habe das eigentlich bessere “kaufmännische Und” durch ein “+” ersetzt, da WordPress das ständig eigenmächtig in das HTML-Äquivalent umgesetzt hat.

Damit ist der Trick nun klar, oder? Ich füge in den Zeilen 3 und 7 einfach zwei Konstanten hinzu, eine für den Modul-/Klassennamen im Modul-/Klassenkopf, und je eine in jeder Sub/Function für den Namen eben dieser. Darauf beziehe ich mich in der Fehlerbehandlung (Zeile 20) und baue die Teilinformationen zu der üblichen Schreibweise zusammen. Die VBA-Zeilennummer erhalten wir in Zeile 21 mittels der generischen Funktion erl (error line). Hat die Zeile mit dem Fehler keine Nummer, liefert diese Funktion eine 0. Wie wir die Zeilennummern hier rein bekommen? Dat krieje mer späder.

In Klassenmodulen können wir auf “CLASS_NAME” theoretisch verzichten, denn da stellt uns VBA den Klassennamen mittels TypeName(Me) zur Verfügung. Aber die Einheitlichkeit hat auch ihre Vorteile, was wir bei der Einbindung der MZ-Tools gesehen haben. (Das wäre dann auch das einzige lizenzpflichtige Tool in diesem Konzept, aber es geht theoretisch auch ohne.)

Jetzt haben wir alles, was wir brauchen, um eine informative Fehlermeldung zusammen zu stellen.

Doch das ist mir noch viel zuviel Schreibarbeit.

Zwischenspeicherung

Um die Informationen zu erhalten und an die aufrufende Stelle weiterzugeben, sollten wir diese in einer übersichtlichen Struktur zusammen stellen. Dazu definiere ich einen Typ und verwende weitgehend die selben Namen wie schon aus dem Err-Objekt bekannt:

Public Type tSavedError
    Description As String
    Source As String
    Number As Long
    Helpfile As String
    HelpContext As String
End Type

Hilfreich wäre nun noch eine Funktion, die uns die ganze Arbeit abnimmt und die Datenstruktur als Ergebnis liefert:

Public Function SaveError( _
    Optional ByVal ClassName As String = "", _
    Optional ByVal FunctionName As String = "", _
    Optional ByVal ErrorLine As Long = 0) As tSavedError

Die Funktion ist nicht weiter kompliziert, aber ich will sie hier nicht im Detail ausbreiten. Sie ist bereits als Teil des Moduls ErrorFunctions.bas in meiner Codebibliothek enthalten. Unter Verwendung dieser Funktion wird unsere Fehlerbehandlung wieder etwas übersichtlicher:

Public Sub Test()

Dim es As tSavedError 
On Error GoTo Catch

42 Debug.Print 1 / 0

Exit Sub

Catch:
es = SaveError(MODUL_NAME, FUNCTION_NAME, erl)

End Sub

Doch was nützt uns das, wenn die Variable danach einfach verpufft? Klar, das Spiel geht noch etwas weiter.

Weitergabe an die aufrufende Instanz

Nachdem die Fehlerbehandlung durch ist, verzweigen wir einfach in einen Teil der aktuellen Sub/Function, der als Abschlussbehandlung bezeichnet werden kann. Ich habe die Labels hierfür übrigens nach einem bekannten Prinzip “catch” und “final” genannt und lediglich auf das “try” verzichtet. Das Prinzip sieht so aus (die Kommentarzeilen mit den Strichen dienen nur der besseren Lesbarkeit):

Public Sub Test()

Dim es As tSavedError 

'----------------------- 
On Error GoTo Catch

' Mein Code

'-----------------------
Final:
On Error Resume Next
' ... Aufräumarbeiten ...

On Error GoTo 0
RaiseSavedError es
Exit Sub

'-----------------------
Catch:
es = SaveError(CLASS_NAME, FUNCTION_NAME, erl)
Resume Final
End Sub

Wie du dir sicher denken kannst, sorgt RaiseSavedError dafür, die Information auf magische Weise an die aufrufende Funktion weiterzugeben. Aber auch dies ist kein Hexenwerk, sondern lediglich ein Ergebnis von err.Raise mit Angabe der bereits gesammelten Informationen. Auch diese Sub ist in der Codebibliothek bereits vorhanden.

Meldung an den User

Nun bleibt von unserer obigen Liste nur noch der letzte Teil übrig, die Meldung an den User. Dafür sorgt ShowSavedError. Diese Sub muss immer dann anstelle von RaiseSavedError aufgerufen werden, wenn keine weitere übergeordnete Instanz mehr vorhanden ist, also z.B. in einem Formular oder Menübefehl. Denn die Laufzeitumgebung darf auf keinen Fall etwas von unseren Tricks erfahren, sonst drängelt sie sich wieder mit diesem langweiligen Fenster in den Vordergrund :-)

Ähnliche Artikel:

Schreibe einen Kommentar

Deine Email-Adresse wird nicht veröffentlicht.

16 + 8 =