QuickStart: Bookmarks



 Sept 2010: v2 of VBA Global Error Handler is now available vbWatchdog (no DLLs!)

Overview

Bookmarks are a new feature introduced with SimplyVBA Global Error Handler v1.2.

In this context, a bookmark is simply a label that you can define in a VB subroutine - the label itself doesn't affect the running code in the local procedure. In this sense, a bookmark is the same as normal VBA labels.

Normal VBA labels are defined like this:

Public Sub Example()
     
    On Error GoTo LocalErrorHandler
	
    ' Code here
    Exit Sub
    
LocalErrorHandler:	' This is a normal VBA line label
    MsgBox Err.Description

End Sub

From a global error handling perspective, normal VB labels are very limited as we can't 'see' these labels from within our global error handling procedure when an error occurs. If we could 'see' the labels and jump to them conditionally from within our global error handler, wouldn't that be cool?...

Bookmarks

Bookmarks allow us to do just that.

Instead of defining normal VB labels, we can instead define ErrEx bookmarks and then conditionally jump to these bookmarks from inside our global error handler by using some extra methods provided by the ErrEx object.

The Basics: Defining Bookmarks

' This is defined in your global error handler module (or any standard module)
Public Const BOOKMARK_ONERROR As Long = 5	

Public Sub Example()
     	
    ' Code here
    Exit Sub
    
ErrEx.Bookmark = BOOKMARK_ONERROR		
    MsgBox Err.Description

End Sub

The first thing you will notice is that there is no 'On Error Goto XYZ' and the reason is simple: When an error occurs, our global error handler will be jumping to the line below BOOKMARK_ONERROR for us, so there is no longer any need for specific local error handling to be set.

ErrEx.Bookmark is a write-only property of data-type Long (numeric). In this example we have chosen a bookmark value of 5 to uniquely identify our bookmark, but you can choose any value for this example as long as the values are unique if you define more than one bookmark.

We suggest that you define bookmarks as constants in a standard VB module - keeping the constants in the same module as your global error handler is probably a good idea.

Jumping to bookmarks from within a global error handler

To jump to a bookmark location from within a global error handler, we use the ErrEx.SetBookmarkAsOnErrorGoto method:

    ErrEx.SetBookmarkAsOnErrorGoto(BookmarkID As Long) As Boolean
Public Sub MyGlobalErrorHandler()

    If ErrEx.SetBookmarkAsOnErrorGoto(BOOKMARK_ONERROR) = True Then

	' The developer has set a BOOKMARK_ONERROR bookmark, so jump to that.
	Exit Sub

    Else

	' The developer hasn't defined a BOOKMARK_ONERROR bookmark in this procedure, 
	' so we will deal with the error here
	ErrEx.State = ErrEx.ShowErrorDialog

    End If

End Sub

In our example BookmarkID is the value BOOKMARK_ONERROR (5). What this call effectively does is set 'On Error Goto BOOKMARK_ONERROR' for the error, and since the error has already occurred (since we are inside a global error handler when calling this), then once your global error handler has completed (Exit Sub), the program flow will be set just beneath the bookmark - at the "MsgBox Err.Description" line.

The return value indicates whether or not the procedure of error actually had that bookmark or not.
        If it returns TRUE then the bookmark exists and program flow will continue to that bookmark upon leaving the global error handler.
        If it returns FALSE then the procedure of error does not have that particular bookmark set and the program flow is not changed.

Determining the current bookmark in the local procedure

From within your global error handler you can also determine the current bookmark that is active. We use the ErrEx.CurrentBookmark property:

    ErrEx.CurrentBookmark As Variant

The return value of this read-only property is the BookmarkID for you to compare to your constant values, or an Empty variant if no bookmark is currently active.

This is particularly useful for writing bookmarks for 'cleanup' routines where for example, you might always want 'On Error Resume Next' to be assumed:

Public Sub Example()
     	
    Dim objRcdSet As DAO.Recordset
    
ErrEx.Bookmark = BOOKMARK_CLEANUP	
	
    objRcdSet.Close 	' Recordset wasn't open - this will cause an exception, but it's ignored
    Set objRcdSet = Nothing	' because we are within the BOOKMARK_CLEANUP bookmark

End Sub
Public Const BOOKMARK_CLEANUP As Long = 6

Public Sub MyGlobalErrorHandler()

    ' Check if the error occurred in the BOOKMARK_CLEANUP section of code
    If ErrEx.CurrentBookmark = BOOKMARK_CLEANUP Then

	ErrEx.State = OnErrorResumeNext
	Exit Sub

    End If

    ' Show the error dialog
    ErrEx.ShowErrorDialog

    If ErrEx.SetBookmarkAsOnErrorGoto(BOOKMARK_CLEANUP) = True Then

	' The developer has set a BOOKMARK_CLEANUP bookmark, so jump to that.
	Exit Sub

    Else

	' The developer hasn't defined a BOOKMARK_CLEANUP bookmark in this procedure
	' so we will end abruptly here
	ErrEx.State = OnErrorEnd    	

    End If

End Sub

Jumping to bookmarks inside our local procedure

To jump to a bookmark location from within a local procedure, we use the ErrEx.GoToBookmark method:

    ErrEx.GoToBookmark(BookmarkID As Long)

In our example BookmarkID is the value BOOKMARK_CLEANUP (6). This call is a replacement for 'Goto XYZ' which is used when working with normal VBA labels.

Public Sub Example()
     	
    Dim objRcdSet As DAO.Recordset

ErrEx.Bookmark = BOOKMARK_CLEANUP
    objRcdSet.Close 		' If the recordset wasn't open, this will cause an exception
    Set objRcdSet = Nothing
    Exit Sub

ErrEx.Bookmark = BOOKMARK_ONERROR		
    MsgBox Err.Description
    Call ErrEx.GoToBookmark(BOOKMARK_CLEANUP)

End Sub

Using error numbers as BookmarkIDs (like try/catch blocks)

Although there are no implicit restrictions on what values you can assign to the constants for BookmarkIDs, we advise using actual error numbers if you want to provide a simplified form of local error handling.

For example, if you want to assign a local error handler for handling a division-by-zero error, we could define it like this:

Public Const BOOKMARK_CLEANUP = -1	' *** Read comments below  ***
Public Const BOOKMARK_ONERROR_DIVBYZERO As Long = 11		' 11 is the ErrEx.Number for division by zero errors
 
Public Sub Example()
     	
    Dim objRcdSet As DAO.Recordset
    Debug.Print 1/0    

ErrEx.Bookmark = BOOKMARK_CLEANUP
    objRcdSet.Close 			' If the recordset wasn't open, this will cause an exception
    Set objRcdSet = Nothing
    Exit Sub

ErrEx.Bookmark = BOOKMARK_ONERROR_DIVBYZERO 
    Resume Next    

End Sub

Public Sub MyGlobalErrorHandler()

    ' Check if the error occurred in the BOOKMARK_CLEANUP section of code
    If ErrEx.CurrentBookmark = BOOKMARK_CLEANUP Then

	' It did - so just ignore it and continue
	ErrEx.State = OnErrorResumeNext
	Exit Sub

    End If

    ' In this example ErrEx.Number will be 11 which is BOOKMARK_ONERROR_DIVBYZERO 
    If ErrEx.SetBookmarkAsOnErrorGoto(ErrEx.Number) = True Then

	' The procedure of error has designated a bookmark for handling this type of error
	Exit Sub

    Else 

	' The error is not handled locally, so we will handle it here...	
	ErrEx.ShowErrorDialog

	If ErrEx.SetBookmarkAsOnErrorGoto(BOOKMARK_CLEANUP) = True Then

	    ' The developer has set a BOOKMARK_CLEANUP bookmark, so jump to that.
	    Exit Sub

    	Else

	    ' The developer hasn't defined a BOOKMARK_CLEANUP bookmark in this procedure, so we will end abruptly here
	    ErrEx.State = OnErrorEnd    	

    	End If

    End If

End Sub

By using BookmarkIDs in this way, you can add new bookmarks without needing to change the global error handler code - just assign the new global constant to the value of an error number and use it in your local error handlers.

*** The only situation you need to be aware of when implementing it this way is that for non-error handling bookmarks (like BOOKMARK_CLEANUP) you must assign values to these bookmark constants that are unlikely to ever be valid error numbers (to avoid number conflicts). We advise using low negative numbers as these are rarely used for VBA error numbers.

A practical example

Let's take an example:

We want to deal with a specific type of error locally (rather than in the global handler) and also ensure that all local variables are properly cleaned up.

Prior to using bookmarks we might have written something like this:

Public Function ChangeProperty(stPropName As String, PropType As DAO.DataTypeEnum, vPropVal As Variant) As Boolean
     
    On Error GoTo LocalErrorHandler

    Dim Db As DAO.Database
    Dim Prop As DAO.Property

    Set Db = CurrentDb
    Db.Properties.Delete stPropName
    Db.Properties.Delete stPropName  ' Causes an error because the property doesn't exist
    Set Prop = Db.CreateProperty(stPropName, PropType, vPropVal, True)
    Db.Properties.Append Prop

    ChangeProperty = True
    
Cleanup:
    On Error Resume Next      ' Ignore errors when cleaning up.
    Set Prop = Nothing
    Set Db = Nothing
    Exit Function

LocalErrorHandler:
    ' Handle local specific errors here:
        Const conItemNotFoundInCollectionError As Long = 3265
        Select Case Err.Number
        
            Case conItemNotFoundInCollectionError
                ' We will ignore the 'property does not exist' error
                Resume Next
                
        End Select
       
    ErrEx.CallGlobalErrorHandler ' Call the global error handler to deal with unhandled errors
    Resume Cleanup        ' After showing the error dialog, we will clean up
 
End Function

By using the new bookmarks features of the SimplyVBA Global Error Handler, we can now cut down this code considerably:
(Assume the global error handler is set up as per Using error numbers as BookmarkIDs)

Public Const BOOKMARK_CLEANUP As Long = -1 ' -1 to avoid conflicts with normal error numbers    
Public Const BOOKMARK_ONERROR_ITEMNOTFOUNDINCOLLECTION As Long = 3265

Public Function ChangeProperty(stPropName As String, PropType As DAO.DataTypeEnum, vPropVal As Variant) As Boolean
     
    Dim Db As DAO.Database
    Dim Prop As DAO.Property

    Set Db = CurrentDb
    Db.Properties.Delete stPropName
    Db.Properties.Delete stPropName ' Causes an error because the property doesn't exist
    Set Prop = Db.CreateProperty(stPropName, PropType, vPropVal, True)
    Db.Properties.Append Prop

    ChangeProperty = True
    
ErrEx.Bookmark = BOOKMARK_CLEANUP
    Set Prop = Nothing
    Set Db = Nothing
    Exit Function

ErrEx.Bookmark = BOOKMARK_ONERROR_ITEMNOTFOUNDINCOLLECTION
    Resume Next
 
End Function

Also check out the BookmarksSample.mdb included with v1.2 and above of the installed application.

Verifying Bookmarks at compile time

Unlike normal VB labels, Bookmarks are not checked at compile time. For this reason, we have provided a simple tool in the Add-ins menu that can be run after compiling to ensure that;

1) There are no duplicate bookmarks defined in the same local procedure.

2) If ErrEx.GoToBookmark is used in a local procedure, then the bookmark that is referenced by it has actually been defined.

This tool only does a very simplistic text based check of the source code, but this is sufficient in most circumstances.

To run the tool, go to the VBE Window, Add-ins menu > SimplyVBA Global Error Handler > Verify Bookmarks Usage