Home
Coding Horror
Compatible
Line Numbers

Zero-in On Errors
with Line Numbers

C PROGRAMMERS OUT THERE will be familiar with the macros __FILE__ and __LINE__; these are expanded by the pre-processor and they allow a precise way to nail down a location within the source code. This makes it easy to report errors:

if (nRCode = DoSomething() < 0)
    fprintf(stderr,
        "DoSomething failed with %d at line %ld of %s\n",
        nRCode, __LINE__, __FILE__);

This kind of macro expansion is absent from Visual Basic, but VB does have features to provide us with similar information. One such feature, the Err object, you doubtless know about already, so I'm not going to say too much about it here. Err contains information about the most recent runtime error, but it also reports the object that caused the error – assuming the object's programmer bothered to supply this information.

The modern Visual Basic programmer, of course, codes everything with classes (forms are classes too) and can mimic C's __FILE__ by reporting TypeName(Me), while for simple BAS files, a module-level string constant will do. What many Visual Basic programmers don't know (because it's undocumented in VB4 and VB5) is that they also have access to the line number using the undocumented global function Erl. This requires qualification, since you'll quickly notice that Erl  always seems to report 0. Erl reports the 'last line number encountered' – and if you hadn't already guessed, the line numbers we're talking about are the old-fashioned BASIC kind: we have to type them ourselves.

If VB doesn't encounter any line numbers, Erl reports 0. This is okay since our error handlers can report the line number and they'll fail gracefully if our lines aren't numbered. It also means we don't need to number every line of code (a good thing, as VB is choosy about which lines can be numbered), because putting only a single line number in the middle of each function will give us 50% better reporting. For example, let's say we label the mid-point of our function with line number 10. If VB reports an error at line 0, the error happened before the label was encountered; if VB reports line 10, the error happened at or beyond the labelled line. Here's the kind of code I usually use to report an error:

    sMsg = "Error (" & Err.Number & ") occurred "
    sMsg = sMsg & "at or after line " & _
               IIf(Erl = 0, "(unspecified)", Erl)
    sMsg = sMsg & " of " & TypeName(Me) & vbCrLf & vbCrLf
    sMsg = sMsg & Err.Description & vbCrLf
    MsgBox sMsg, vbOkOnly + vbExclamation, App.Title

There are a couple of things to say about this. The first is that we're using TypeName to pin down the unit of source code we're in, so we need to make sure we're using unique line numbers within the source file. That's tiresome, but Visual Basic no longer demands it as it did in VB3. In practice it's much more convenient to label lines arbitrarily and also report the function we're in, although this requires that we keep track of the current function. There are several ways to do this, but we're not going to talk about them here.

The second problem relates to more complex error handling. As we all know by now, it's often necessary inside an error handler to save and restore the Err object's values. If you're not convinced, here's a quick example of when we might need to do this:

    Public Sub SomeMethod()
        ...
    ErrBlock:
        Unload Me ' Oops - this wipes out Err
        Err.Raise Err.Number
    End Sub

In this case the Form_Unload and Form_QueryUnload events will have error handling (because event handlers must) and hence will reset the Err object. The Err.Raise will fail because we're not allowed to Raise error 0.

Okay, so now we write SaveErr and RestoreErr functions, something like this (abbreviated):

    Private m_nNumber As Long
    Private m_sDescription As String

    Public Sub SaveErr()
        m_nNumber = Err.Number
        m_sDescription = Err.Description
        ' ...etc
    End Sub

    Public Sub RestoreErr()
        Err.Number = m_nDescription
        Err.Description = m_sDescription
        ' ...etc
    End Sub

This lets us sort out the original error handler like this:

    Public Sub SomeMethod()
        ...
    ErrBlock:
        SaveErr
        Unload Me
        RestoreErr
        Err.Raise Err.Number
    End Sub

This works fine, but there's a problem if we're using line numbers. For example, our original error handler might actually look more like this:

    Public Sub SomeMethod()
        ...
    ErrBlock:
        SaveErr
        Unload Me
        RestoreErr
        Report Err.Number, Err.Description, Erl   ' <-- uh-oh
    End Sub

The problem becomes apparent when we try to add the statement Erl = m_nErl to our RestoreErr function. VB won't allow this, because Erl is (or at least behaves like) a function, not a variable.

There are two ways to overcome this, one elegant and one almost unspeakably inelegant. I'm going to show you both ways because VB3 users (yes, they still exist) have to do it the unspeakable way.

Problem: how to find a way of assigning an arbitrary value to Erl.

First, the old way. In the interests of clarity I've written the code using VB5/VB4 syntax, but the same idea works in VB3 with the following two differences. First, while VB3 allows us to save the error number by assigning to the Err variable (not to be confused with VB4 and VB5's Err object), like VB4 and VB5 it doesn't allow the same trick with Erl. Second, VB3 doesn't provide any way to preserve the error message – this isn't so bad, as setting Err also reinstates the error message, only without any parameter information that may have been embedded in the original.

Solution 1 (VB3): write a function with line numbers in it, make an error happen on the nominated line and ignore it.

For illustration purposes this function only works for Erl values up to 20. In practice we'd probably need to code 1000 lines in here; alternatively we could make the assumption that only units of 10 (10, 20, 30,...) are used and code only these lines. Unfortunately, line numbers in VB3 have to be unique within a source file, so we need to think carefully about how many lines we're likely to have in a file. If we were going to code 1000 lines here we'd have to find an alternative to the On...Goto because we'd overflow the line length (don't you just love VB?).

Private Sub SetLineNum(ByVal nLineNum As Integer)
    ' Effectively does Erl = nLineNum

    On Error Resume Next
    On nLineNum GoTo 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, _
                     11, 12, 13, 14, 15, 16, 17, 18, 19, 20

1   Err.Raise 1: Exit Sub
2   Err.Raise 1: Exit Sub
3   Err.Raise 1: Exit Sub
4   Err.Raise 1: Exit Sub
5   Err.Raise 1: Exit Sub
6   Err.Raise 1: Exit Sub
7   Err.Raise 1: Exit Sub
8   Err.Raise 1: Exit Sub
9   Err.Raise 1: Exit Sub
10  Err.Raise 1: Exit Sub
11  Err.Raise 1: Exit Sub
12  Err.Raise 1: Exit Sub
13  Err.Raise 1: Exit Sub
14  Err.Raise 1: Exit Sub
15  Err.Raise 1: Exit Sub
16  Err.Raise 1: Exit Sub
17  Err.Raise 1: Exit Sub
18  Err.Raise 1: Exit Sub
19  Err.Raise 1: Exit Sub
20  Err.Raise 1: Exit Sub

    ' ...ad infinitum

End Sub

Now we can add to our SaveErr and RestoreErr functions:

Public Sub SaveErr()
    m_nNumber = Err.Number
    m_sDescription = Err.Description
    m_nErl = Erl
End Sub

Public Sub RestoreErr()
    Err.Number = m_nDescription
    Err.Description = m_sDescription
    SetLineNum m_nErl ' Erl = m_nErl
End Sub

If you know of a more grotesque hack, I'd like to hear about it!

Solution 2 (VB4 onwards): Introduce a public variable Erl that overrides the VB Erl function.

Visual Basic 4 and 5 allow us to override built-in functions by adding our own functions with the same names. This is sometimes called 'subclassing', and it's why we can write our own MsgBox function to enhance the built-in one, say by adding better defaults:

Public Function MsgBox( _
    Optional ByVal Prompt As String = "", _
    Optional ByVal Buttons As VbMsgBoxStyle = _
                     vbOKOnly + vbExclamation, _
    Optional ByVal Title As String = "", _
    Optional ByVal Helpfile As String = "", _
    Optional ByVal Context As Long = 0) As VbMsgBoxResult

    If Title = "" Then Title = App.Title
    VBA.MsgBox Prompt, Buttons, Title, Helpfile, Context
End Function

This same feature means we can hide the Erl function and replace it with our own variable. All we need to do is define a public variable Erl somewhere:

    Public Erl As Integer

Now we must arrange to execute the following line of code whenever we trap an error:

    Erl = VBA.Erl

Although this isn't entirely satisfactory because there's no way to keep the new Erl variable up to date automatically, usually it's just one more line to add to a standard error handling template. Now we can rewrite our SaveErr and RestoreErr functions like this:

Public Sub SaveErr()
    m_nNumber = Err.Number
    m_sDescription = Err.Description
End Sub
Public Sub RestoreErr()
    Err.Number = m_nDescription
    Err.Description = m_sDescription
End Sub

Notice that we don't need to bother with saving and restoring Erl in these functions.

Article © 1998 VBUG. Reproduced with permission.

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009                             sign the guest book