I’m still refining my collection of best practices. Thanks to Lars Berntrop-Bos for a suggestion I incorporated here.
The handling of unexpected errors is one of the headaches we have to deal with while coding. By their nature, you don’t have a specific strategy for handling that error, or it would be an expected error. But you don’t want to present the end user with the uninformative default error dialog.
We’d hope the end user never sees the system error message, but if they do, we should at least have enough information for a developer to locate the code line that generated the error. That means we want a stack with line numbers, such as Java generates by default.
In this article, I discuss three “levels” of error handling. You can choose how far to take it based on your needs and the amount of effort you want to put in.
Level 1: the basic trap
LotusScript doesn’t have a built-in mechanism for capturing the stack at time of error, so we have to roll our own. This means each function, sub, or class property that could conceivably generate an error condition, needs code to trap that error and record the error location, then “throw” that same error. The calling module can do the same, adding its own details. If someone up the stack has more specific handling for that error, well and good. But if not, the resulting default error message now at least contains the stack.
Here’s the code you can insert in each module to do this:
Sub Zardoz
Dim i%, roger$
On Error Goto TRAP
...
Exit Sub ' or Function or Property
TRAP: Stop
Error Err, Error & { //} & Getthreadinfo(1) & {:} & Erl
End Sub
Before the first line of functional code, use On Error to establish default error handling. The module might contain additional On Error statements to deal with specific errors, or to change the default at a later point.
In the error trapping code, use GetThreadInfo(1) to get the subroutine or function name. I prefer this to hard-coding the name in the Error statement because it’s less work — you can just paste the same code into each module (or automate its insertion — see below). If you rename or copy/paste and modify the function, you have one fewer occurrence of the name to update, and if the application is translated, it’s one less potential overtranslation.
The name GetThreadInfo returns is uppercase, which is ugly, but it’s enough to let you know where the error is, so it doesn’t seem to me worth great effort to change.
The Stop statement helps with debugging. Normally, the LotusScript debugger halts on the line that caused the error. Since the code traps this error, the debugger doesn’t halt to let you debug at that point. It won’t stop until the error bubbles up to a module that doesn’t trap it. By then all the lovely local variable information on the stack is gone.
With the Stop statement, the debugger will stop in the module where the error originated. You can’t tell which line it was on (at least not until you see what line number is in the error message). But at least you are in the vicinity and have variables and a list of callers to examine.
If you find this website helpful and want to give back, may I suggest buying, reading, and reviewing one of my excellent books? More are coming soon!
If you want email whenever there’s a new post, you can subscribe to the email list, which is 100% private and used only to send you information about stuff on this site.
Can’t we automate this more?
At present there’s no way to do away with the need for error trapping code in each module, though this would certainly be a welcome change to LotusScript.
I do have a program you can point at a script library and say, “add error trapping to all modules.” So you can write the code initially without error trapping and have it automatically added later. This works great, but I’m not quite ready to release it yet.
What about classes?
The above is fine for stand-alone subs and functions, but what about a class method? One wants the stack information to include the name of the class that contains the method, not just the method name.
Erl treats all classes as part of the Declarations section, so the line number it returns isn’t relative to the beginning of the module, but to some point higher up. You can find the line in the Eclipse-based editor using Ctrl+Alt+L (menu Navigate > Go to Erl Line…), but that assumes the code you’re editing is the same version of code that generated the error. Sometimes you’re working from a user report about an earlier version of the application.
Here’s how to modify the error trap within a class method to meet those needs:
...
Exit Property ' or Sub or Function
TRAP: Stop
Error Err, Error & { //} & Typename(Me) & {.} & Getthreadinfo(1) & {:} & Erl & (Erl-Getthreadinfo(0))
End Property
Getthreadinfo(0) returns the line number of the current line of code — the Error statement. Now you have:
- the class name of the object, returned by Typename
- the function name, from Getthreadinfo(1)
- the “raw” error line number from Erl
- the line number of the error relative to the Error statement, calculated using Erl and Getthreadinfo(0).
If the error is “Unrecognized rotor setting ‘green’. //ROTORCONTROL.SETANGLE:450-8//CLICK:15” the error happened in the SetAngle method of RotorControl, 8 lines above the Error statement (or line 450 for the Go to Erl Line dialog).
Caveat: the Typename shown in the message isn’t necessarily the class that contains the problem code. If using derived classes, the actual code may be in the base class. When that code asks for the object’s Typename, it gets the type of the current object, which may be of a derived class.
Since a class can override the methods of its base class, you might have multiple modules with the same name. It’s generally not hard to figure out which of them threw the error — especially since you have the error line number to work with — but it can look odd when you get, for instance:
Some message //DXLCell.AddStyledPar:782-5 //DXLCell.AddStyledPar:890-13
Is this recursion? No. The original site of the error in this case was the AddStyledPar method not of DXLCell, but of the base class. That method was explicitly called from the overridden method of the same name in DXLCell, e.g.:
Class DXLContainer
...
Function AddStyledPar(style) As DXLPar
...
End Class
Class DXLCell As DXLContainer
...
Function AddStyledPar(style) As DXLPar ' overrides base class method
...
Set par = DXLContainer..AddStyledPar(style)
DXLCell class is derived from DXLContainer, and in this case the AddStyledPar method made a call to the base class method it’s overriding. When the error occurred in the method DXLContainer..AddStyledPar, it happened in the context of a DXLCell object, so that’s what Typename returned.
Level 2: Turning off error handling while debugging
One problem with trapping errors: it makes the code harder to debug with the LotusScript debugger. As discussed above, the Stop statement in each error trap makes sure we at least stop in the right subroutine, but we can’t tell what line threw the error.
What we’d like is, if the LotusScript debugger is turned on, to revert to the default handling for unexpected errors — halt and debug on the actual line where the error occurred.
You can detect whether the debugger is running — the way to do it is with timing. Somewhere, in a library that’s used universally in your application, you test for this.
'(Globals) - Declarations
Public BGlobalTrapErrs As Integer ... Sub Initialize Dim st As Single st = Timer Stop' if it takes much time to pass this line, debugger is on.
BGlobalTrapErrs = Timer-st < .1 ... End Sub
The Initialize code runs when the library is loading, so any code that loads this library will automatically have BGlobalTrapErrs set True if the debugger is not active, False otherwise.
(This is a Boolean value, but I declared it Integer for performance. Booleans are a tiny bit slower than Integers. Normally I would opt for the “clarity” side of this clarity vs. performance tradeoff, but this variable is referenced a lot).
Within each module that might conceivably generate an error, we modify the On Error statement as follows:
If BGlobalTrapErrs Then On Error Goto TRAP
If we’re debugging, the On Error statement doesn’t execute, so the error is handled in the default way; the debugger will highlight the actual error line.
One challenge to this approach is that you have to standardize on the name of the global variable throughout your applications. If you design your script libraries well, they’re reusable, and it’s best if you can reuse them unaltered in different applications. That isn’t the case if they refer to a global that’s not defined in those applications.
While I have used this approach, I don’t think I’ll do it regularly. I just make sure to copy the stack information the first time, so when I debug I already know what line to look at.
Level 3: OK, but end users don’t take screenshots of errors
In our dreams, when someone gets an application error, they always report it, giving the detailed series of steps to reproduce the problem and including a copy of the error text.
Reality, however, is a different story. “Such-and-such didn’t work” is often the level of detail you get in a problem report. That’s why some developers create solutions to log unexpected errors so they’re available for later reference.
I think this is a great idea, though it’s kind of complex, so it’s not for everyone. I want to see it done in a way that’s reusable and has minimal impact on the code. If there are reusable libraries, I don’t want for all those libraries to have to know about the error logging code for the particular application. If they do, it prevents you from making effective use of open source libraries — like the ones I publish — unless you’re willing to do a lot of edits to them and keep editing them anytime a new version is available. Even within a single company, it’s hard to get everyone lined up using the same error handling system in all their libraries, especially when different departments may have different IT procedures in place for tracking those errors.
So, while I think recording errors and having a central collecting point for them is a good idea, I prefer to do it just at the top level — instead of a logging library every other piece of code has to know about and include, let’s have a best practice of inserting error trapping to record the stack as part of the error message, as described above. Only the top level of code — the agent, or “Click” event, or whatever starts the process off — needs to know about the logging system used in that application. That top-level calling code defines an error trap similar to the above, except instead of re-throwing the error, it passes the error information into a logging function.
That function could parse the stack information (or just include it in its original form for humans to parse). It would probably put up a dialog inviting the end user to explain what they were doing (they won’t tell, but it doesn’t hurt to ask), and include that information with the log entry. The details can be written in the local log.nsf, and also deposited in a shared error logging system, probably by mailing it to a mail-in database (or in a small shop, mailing it directly to the developer. 🙂 ).
Details on how to implement this are beyond the scope of what I want to cover today, and the details will also vary depending how you do things in your organization, and anyway Lars is writing about that. But I’ll have more to say about it, so watch this blog.
I advise against wwiting anything after the colon. I’ve seen the debugger desync on having multiple items on a line separated with a colon, by which I mean that the current line displayed in the debugger is not the line being executed. Very confusing. Also, Erl, the error line number, is affected. Also confusing.
Hi. If we are talking about error handling, we should remember LsError library (See http://lserror.sourceforge.net for further information).
The point is that there are standard functions rethrow and notify, which respectively collect error stack and show error to user. Ideology is the following: in the root code you call the standard error output dialog; in all service/library ones you just collect the stack.
Processing comes down to two lines:
on error goto catch ” or if( debugError )then on error goto catch if you want to disable global variable error handling
…
catch: rethrow
And that’s it! It works, and it’s convenient.
I modified the original library a bit (more exactly, rewrote it to my liking, and also added standard global variables for the current session, database, document, agent):
Public Sub rethrow
If( Err=0 )Then Exit Sub
Print “rethrow”, GetThreadInfo(10) &”.” &Erl &” –> ” +Error$
Error Err, GetThreadInfo(10) &”.” &Erl &” –> ” +Error$ ” проброс наверх с формированием цепочки вызовов
End Sub
Function errorDlg(fName As String)
errorDlg= errorShow(fName)
If( errorDlg )Then End -1
End Function
Function errorShow(fName As String) As Boolean ” если вызывающей проге нужно не просто закрыться, а прибрать за собой
If Err=0 Then Exit Function
If( fName=”” And Not(agCurr Is Nothing) )Then fName= agCurr.Name
fName= fName +”(” +ss.CommonUserName +”, ” &Now &”)”
If Err=1001 Then
Print fName, Error
MessageBox Error, 64, fName
Else
Print fName, “Ошибка ” & Err & ” в строке ” & Erl & “: ” & Error
MessageBox “Ошибка ” & Err & ” в строке ” & Erl & “: “+Chr(13) & Error, 64, fName
End If
errorShow= True
End Function
form action example:
Sub Click(Source As Button)
On Error Goto catch
…
catch:errorDlg “action Send to exec”
End Sub
Function sendNotify As Boolean
On Error Goto catch
…
catch:rethrow
End Function
агент экспорта в excel:
Sub Click(Source As Button)
On Error Goto catch
…
catch:if( errorShow(“”) )then Resume exitLool
exitLool:
On Error Resume Next
xlApp.visible= True
End Sub
I downloaded the project and am looking at it. There are a few things I don’t agree with there, but the two most important are:
1. It assumes you’re running an agent and causes an error itself if not.
2. You can’t use just two lines — you have to explicitly exit before the error trap line. Yes, I know rethrow checks the value of Err and exits if it’s zero, but that’s not reliable — Err will not be zero if there’s an expected error that the calling function handles or ignores.
Mikle, I see email isn’t getting through to you because of full mailbox.
The essential idea is that the message from the errorhandler of each procedure is gradually added to Error.
I found it somewhere and we have been using it for a long time.
But we supplemented it with a list of important local variables of each procedure. The function is called with 2 parameters: the name (it cannot be determined automatically) and the expression for obtaining the value (usually just a variable, but there can be something more complex). It doesn’t matter that there may be another error while doing this, because that’s in On Error Resume Next (the current error must be written before the variables).
Hi there,
very interesting discussion. But I have a question concerning the following problem:
Is there any way in LS to determine the name of the object (variable) in the event of an error in an LS class method?
For 1 class = 1 object it is of course trivial, but later if a class has during runtime more than 2 class objects, it becomes difficult.
Kind Regards,
Joe Herrmann
I am talking about user classes, of course 😉
An object can be referenced by different variables within the same module and different names in different levels of the code when it’s passed as a parameter. If you trap and display line numbers as shown here, it should be reasonably obvious which variable is being referenced — it’s the one on that line.
Occasionally, there might be more than one occurrence of a property or method invocation involving variables of the same class on the same line of Code. That seems likely to be rare, but if it happens, I suppose you could always break it into multiple lines so that when the error occurred, it would have a unique line number associated with a particular occurrence of the object variable.
Usually, I find that once I know where the error is occurring, the stupid thing I did to cause it is pretty evident.
Yes, I can have various references (variables) to an object. That makes it more complicated. If I have a class with many instances (not references to the same object) it becomes difficult.
I will therefore have to assign a unique identification to each object as a property.
I agree assigning a unique identifier to an object is one last-ditch way to identify the source of the error. I’ve never found it necessary to resort to that myself. What’s your situation that the stack with line numbers isn’t enough?