Fehlerbehandlung in VBA: Patterns für die Praxis

Eine Prozedur, die monatelang unauffällig ihren Dienst getan hat, bricht plötzlich mit einer kryptischen Meldung ab – und der Anwender steht vor einem Dialog, mit dem er nichts anfangen kann. Der Grund ist fast immer derselbe: Es gibt keine oder nur eine halbherzige Fehlerbehandlung. Dabei ist “On Error” eines der mächtigsten Werkzeuge in VBA, wenn man weiß, wie man es einsetzt. Wir schauen uns die Muster an, die aus einer fragilen Lösung eine robuste machen: wie wir Fehler nicht nur abfangen, sondern strukturiert behandeln, Ressourcen zuverlässig freigeben, Fehler zwischen den Schichten einer Anwendung weiterreichen und sie in einer Tabelle protokollieren. Die Beispiele findest Du im Modul “mdlFehlerbehandlung” der Beispieldatenbank. Wie das im Einzelnen gelingt, zeigt der vorliegende Beitrag.

Was passiert ohne Fehlerbehandlung?

Schauen wir uns zuerst an, was VBA von sich aus tut, wenn ein Laufzeitfehler auftritt.

Die Prozedur DateiOhneSchutz aus folgendem Listing 1 öffnet eine Textdatei, die es gar nicht gibt:

Public Sub DateiOhneSchutz()
    Dim intFile As Integer
    intFile = FreeFile
    Open "C:\NichtVorhanden.txt" _
        For Input As #intFile
    Close #intFile
End Sub

Existiert die Datei nicht, bricht VBA mit dem Laufzeitfehler 53 ab und zeigt den Dialog aus Bild 1 – mit einer Meldung, die dem Anwender nichts sagt, und mit den Schaltflächen Beenden und Debuggen, die in einer ausgelieferten Anwendung beide nichts zu suchen haben.

Der Standarddialog von VBA beim Laufzeitfehler 53 - so etwas darf der Anwender nie zu sehen bekommen

Bild 1: Der Standarddialog von VBA beim Laufzeitfehler 53 – so etwas darf der Anwender nie zu sehen bekommen

Klicken wir auf Debuggen, sehen wir die farbig hinterlegte fehlerhafte Anweisung (siehe Bild 2).

Anschließend wird die fehlerhafte Zeile farbig hinterlegt.

Bild 2: Anschließend wird die fehlerhafte Zeile farbig hinterlegt.

Zwei Dinge sind hier schiefgelaufen: Der Anwender bekommt keine verständliche Meldung, und die Prozedur bricht mitten im Ablauf ab. In anderen Prozeduren können dadurch bereits geöffnete Dateien, Recordsets oder andere Ressourcen unaufgeräumt zurückbleiben.

Die drei Formen von On Error

VBA kennt genau drei Varianten der On Error-Anweisung, und sie bestimmen, was bei einem Laufzeitfehler geschieht:

  • On Error GoTo Marke springt zur angegebenen Sprungmarke. Das ist die Standardform für eine strukturierte Fehlerbehandlung.
  • On Error Resume Next ignoriert den Fehler und macht mit der nächsten Anweisung weiter. Das ist nützlich, wenn wir den Fehler unmittelbar danach selbst prüfen wollen.
  • On Error GoTo 0 schaltet jede aktive Fehlerbehandlung in der aktuellen Prozedur ab. Ein Fehler führt danach wieder zum Standarddialog.

Ein Punkt, der später noch wichtig wird: On Error gilt immer nur innerhalb der Prozedur, in der es steht. Eine globale Fehlerbehandlung gibt es in VBA nicht – jede Prozedur muss sich selbst schützen.

Das Standard-Pattern: On Error GoTo

Das bewährteste Muster sieht so aus: Am Anfang der Prozedur leiten wir auftretende Fehler auf eine Sprungmarke um, am Ende springen wir über den Fehlerblock hinweg. Bauen wir unser Beispiel entsprechend um – das Ergebnis heißt DateiMitSchutz und steht in folgendem Listing:

Public Sub DateiMitSchutz()
    On Error GoTo ErrHandler
    Dim intFile As Integer
    intFile = FreeFile
    Open "C:\NichtVorhanden.txt" For Input As #intFile
    ' ... Datei verarbeiten ...
    Close #intFile
ExitHere:
    Exit Sub
ErrHandler:
    MsgBox "Fehler " & Err.Number & ": " _
        & Err.Description, vbCritical, "DateiMitSchutz"
    Resume ExitHere
End Sub

Der Ablauf im Einzelnen: On Error GoTo ErrHandler aktiviert die Fehlerbehandlung – ab hier springt VBA bei einem Laufzeitfehler zur Marke ErrHandler.

Läuft alles glatt, erreichen wir ExitHere und verlassen die Prozedur mit Exit Sub. Tritt dagegen ein Fehler auf, landen wir im ErrHandler, zeigen eine Meldung an und springen mit Resume ExitHere zur Austrittsmarke.

Warum eigentlich Resume ExitHere und nicht einfach GoTo ExitHere? Der Unterschied sieht nach Geschmackssache aus, ist aber entscheidend: Resume meldet VBA, dass der Fehler behandelt wurde. Erst danach kann überhaupt wieder ein Fehler abgefangen werden. Mit GoTo bleibt die Prozedur dagegen im Fehlerzustand.

Den Fehlerzustand sichtbar machen

Das ist eine Behauptung – schauen wir sie uns an, statt sie zu glauben. Die Prozedur GoToStattResume in macht den Unterschied im Direktbereich sichtbar:

Public Sub GoToStattResume()
    On Error GoTo ErrHandler
    Err.Raise 53
ExitHere:
    Debug.Print "In ExitHere: Err.Number = " & Err.Number
    Exit Sub
ErrHandler:
    Debug.Print "Im Handler:  Err.Number = " & Err.Number
    GoTo ExitHere
End Sub

Rufe die Prozedur im Direktbereich mit GoToStattResume auf. Die Ausgabe zeigt in beiden Zeilen die 53 – der Fehler ist in ExitHere also immer noch aktiv (siehe Bild 3).

Der Direktbereich zeigt es schwarz auf weiß: Nach GoTo steht Err.Number immer noch auf 53

Bild 3: Der Direktbereich zeigt es schwarz auf weiß: Nach GoTo steht Err.Number immer noch auf 53

Tauschst Du das GoTo ExitHere gegen Resume ExitHere, steht in der zweiten Zeile eine 0: Der Fehler gilt als erledigt.

Und genau daran hängt die Praxisfolge – ein weiterer Fehler in der Aufräumlogik würde in der GoTo-Variante nicht mehr abgefangen, sondern führte direkt zum Standarddialog.

Das Err-Objekt im Detail

Das Err-Objekt ist immer verfügbar und beschreibt den zuletzt aufgetretenen Fehler. Wichtig sind drei Eigenschaften:

  • Number ist die Fehlernummer, wobei 0 für “kein Fehler” steht. Für eigene Fehler nutzen wir Nummern ab vbObjectError + 512 – dazu kommen wir gleich.
  • Description liefert die Fehlerbeschreibung im Klartext.
  • Source nennt die Quelle: standardmäßig den Namen des VBA-Projekts, bei externen Bibliotheken deren Namen, etwa DAO.Database oder ADODB.Connection.

Dazu kommen zwei Methoden. Clear setzt Number auf 0 und leert die übrigen Eigenschaften des Err-Objekts. Beim Verlassen einer Prozedur und durch eine Resume-Anweisung wird der Fehlerzustand ebenfalls zurückgesetzt. Wenn wir mehrere Operationen unter On Error Resume Next prüfen, sollten wir Err.Clear dagegen ausdrücklich zwischen den Operationen aufrufen.

Und Raise löst einen Laufzeitfehler aus – damit erzeugen wir gleich gezielt eigene Fehler.

Resume – drei Varianten

Aus dem Fehlerblock heraus haben wir drei Möglichkeiten, den Programmfluss fortzusetzen. Resume führt die fehlerauslösende Zeile erneut aus – sinnvoll nur dann, wenn wir die Ursache im Fehlerblock tatsächlich beseitigt haben.

Resume Next macht mit der Zeile danach weiter, wenn der Fehler tolerierbar ist. Und Resume Marke springt zu einer bestimmten Marke, was die häufigste Variante ist: Genau so kommen wir geordnet zur Austrittsmarke.

Wie Resume gezielt eingesetzt werden kann, zeigt das folgende Listing. Die Prozedur OrdnerAnlegen legt einen Ordner an, dessen übergeordneter Ordner womöglich noch fehlt – in dem Fall legen wir diesen an und versuchen es erneut:

Public Sub OrdnerAnlegen(strPfad As String)
    On Error GoTo ErrHandler
    MkDir strPfad
ExitHere:
    Exit Sub
ErrHandler:
    If Err.Number = 76 Then
        'Pfad nicht gefunden: 
        'übergeordneten Ordner anlegen
        MkDir Left(strPfad, InStrRev(strPfad, "\") - 1)
        Resume
    Else
        MsgBox "Fehler " & Err.Number & ": " _
            & Err.Description
        Resume ExitHere
    End If
End Sub

Aufgerufen wird das etwa so:

OrdnerAnlegen "C:\Temp\Export\Kunden"

Zwei Stolpersteine stecken hier drin: Erstens legt die Korrektur nur eine Ebene an – fehlen zwei, scheitert schon das MkDir im Fehlerblock. Ein weiterer Fehler innerhalb eines bereits aktiven Fehlerhandlers kann von demselben Handler nicht erneut behandelt werden. Gibt es keinen übergeordneten aktiven Handler, erscheint der VBA-Standarddialog. Zweitens droht eine Endlosschleife, wenn die Korrektur das Problem gar nicht löst: Resume wiederholt dann immer wieder dieselbe Zeile. Wie wir das mit einem Zähler entschärfen, sehen wir weiter unten.

On Error Resume Next – gezielt und sparsam

On Error Resume Next hat einen schlechten Ruf, und der ist meistens verdient: Wer es als Allzweckwaffe einsetzt, verschluckt Fehler stillschweigend und macht die Anwendung unberechenbar. Es gibt aber Situationen, in denen es genau das richtige Werkzeug ist – nämlich dann, wenn wir den Fehlerstatus einer einzelnen Operation bewusst abfragen wollen.

Ein typischer Fall: Wir wollen wissen, ob ein Eintrag in einer Collection existiert. Eine Exists-Methode gibt es dort nicht, also nutzen wir den Zugriffsfehler als Prüfmittel. Das erledigt die Funktion ExistiertInCollection. Sie erwartet die zu prüfende Collection und den Schlüssel und liefert True oder False zurück:

Public Function ExistiertInCollection(col As Collection, _
        strKey As String) As Boolean
    Dim varDummy As Variant
    On Error Resume Next
    varDummy = col(strKey)
    ExistiertInCollection = (Err.Number = 0)
    On Error GoTo 0
End Function

Das Muster dahinter ist immer dasselbe: On Error Resume Next einschalten, die kritische Anweisung ausführen, Err.Number prüfen und sofort danach mit On Error GoTo 0 zurücksetzen – oder, wenn die Prozedur eine eigene Fehlerbehandlung hat, mit On Error GoTo ErrHandler wieder auf die reguläre Marke umleiten. Ausprobieren lässt sich das wie folgt:

Dim col As New Collection
col.Add "Wert", "Schluessel"
Debug.Print ExistiertInCollection(col, "Schluessel")
'Liefert True
Debug.Print ExistiertInCollection(col, "Fehlt")    
'Liefert False

Entscheidend ist die Disziplin: On Error Resume Next sollte nie länger als zwei oder drei Zeilen aktiv sein. Alles andere ist ein Wartungsrisiko.

Aufräumen bei Fehlern: das Exit-Pattern

In vielen Prozeduren müssen wir am Ende Ressourcen freigeben – Recordsets schließen, Objektvariablen auf Nothing setzen, Dateien schließen. Diese Arbeit darf der Fehlerblock nicht überspringen.

Deshalb bekommt die Prozedur eine gemeinsame Austrittsmarke, die sowohl der reguläre Ablauf als auch der Fehlerblock anspringt. Wie das aussieht, zeigt die Prozedur DatenExportieren (siehe Listing 1). Sie liest die Tabelle tblKunden aus und schreibt die Namen in eine Textdatei.

Public Sub DatenExportieren(strPfad As String)
    Dim db As DAO.Database
    Dim rst As DAO.Recordset
    Dim intFile As Integer
    Dim bolDateiOffen As Boolean
    On Error GoTo ErrHandler
    Set db = CurrentDb
    Set rst = db.OpenRecordset("SELECT KundenName FROM tblKunden", dbOpenSnapshot)
    intFile = FreeFile
    Open strPfad For Output As #intFile
    bolDateiOffen = True
    Do Until rst.EOF
        Print #intFile, rst!KundenName
        rst.MoveNext
    Loop
ExitHere:
    If bolDateiOffen Then
        Close #intFile
        bolDateiOffen = False
    End If
    If Not rst Is Nothing Then
        rst.Close
        Set rst = Nothing
    End If
    Set db = Nothing
    Exit Sub
ErrHandler:
    MsgBox "Fehler in DatenExportieren:" & vbCrLf _
        & Err.Number & " - " & Err.Description, _
        vbCritical
    Resume ExitHere
End Sub

Listing 1: Beispiel für das Exit-Pattern

Der Aufruf sieht so aus:

DatenExportieren "C:\Temp\Kunden.txt"

Beachte die Prüfung If Not rst Is Nothing: Der Fehler kann auch schon vor dem OpenRecordset auftreten – dann wäre rst noch Nothing, und ausgerechnet das Aufräumen würde einen neuen Fehler auslösen. Ob die Datei tatsächlich geöffnet wurde, halten wir in der Variablen bolDateiOffen fest. Eine von FreeFile gelieferte Dateinummer allein beweist noch nicht, dass die anschließende Open-Anweisung erfolgreich war.

Fehler weiterreichen mit Err.Raise

Nicht jeder Fehler gehört dort behandelt, wo er auftritt. In einer sauber aufgebauten Anwendung gibt es Schichten: Datenzugriff, Geschäftslogik und Benutzeroberfläche. Eine Datenzugriffsfunktion soll dem Aufrufer melden, dass etwas schiefgelaufen ist – aber keine MsgBox anzeigen. Dafür ist Err.Raise da.

Die Funktion KundeNachID liest zu einer Kundennummer den Namen aus tblKunden. Findet sie keinen Datensatz, löst sie selbst einen Fehler aus; tritt ein anderer Fehler auf, räumt sie auf und reicht ihn nach oben weiter (siehe Listing 2).

Public Function KundeNachID(lngID As Long) As String
    On Error GoTo ErrHandler
    Dim db As DAO.Database
    Dim rst As DAO.Recordset
    Dim lngNummer As Long
    Dim strQuelle As String
    Dim strText As String
    Set db = CurrentDb
    Set rst = db.OpenRecordset("SELECT Firma FROM tblKunden WHERE KundeID = " & lngID, dbOpenSnapshot)
    If rst.EOF Then
        Err.Raise vbObjectError + 1001, "KundeNachID", "Kein Kunde mit der ID " & lngID & " gefunden."
    End If
    KundeNachID = rst!Firma
ExitHere:
    If Not rst Is Nothing Then
        rst.Close
        Set rst = Nothing
    End If
    Set db = Nothing
    Exit Function
ErrHandler:
    ' Fehlerdaten sichern, bevor wir aufräumen
    lngNummer = Err.Number
    strQuelle = Err.Source
    strText = Err.Description
    If Not rst Is Nothing Then
        rst.Close
        Set rst = Nothing
    End If
    Set db = Nothing
    Err.Raise lngNummer, strQuelle, strText
End Function

Listing 2: Fehler weiterreichen mit Err.Raise

Hier geschehen zwei verschiedene Dinge. Findet die Abfrage keinen Kunden, erzeugen wir mit Err.Raise einen eigenen Fehler.

Access, SQL und Cloud Automation

Unser exklusives Angebot für Dich!

VB-Entwickler
12,50 € im Monat*

(Gilt für den Abschluss eines Jahres-Abonnements.)

Hier geht’s weiter →

Die ersten 4 Wochen kostenlos testen – voller Zugriff auf alle Artikel, vollständigen Code und Beispieldatenbanken. Kein Risiko: Wenn es nicht passt, kündigst Du einfach innerhalb der ersten vier Wochen.

PayPal VISA Mastercard SEPA
Kostenlos & unverbindlich

Oder hast Du eine konkrete Frage zu Deiner eigenen Access-Anwendung?

Vielleicht stellt Deine Anwendung Dich vor eine Herausforderung, zu der Du bisher keine Lösung findest. Schlechte Performance, kein ausreichender Zugriffsschutz, Du bist unsicher über Dein Datenmodell oder Dein Code liefert unerklärliche Fehler?

In unserem kostenlosen Access-Audit schaut sich André Minhorst persönlich gemeinsam mit Dir Deine Lösung per Zoom an – und zeigt Dir, wo Datenmodell, VBA-Code, Ergonomie und Sicherheit Optimierungspotenzial bieten.

Jetzt kostenloses Access-Audit anfordern →