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. |