Debugging PracticesA couple of articles have recently been posted in the Iris Today on-line magazine talking about debugging LotusScript. Here are links to part 1 and part 2. We thought it would be a good time to reference those articles and talk about what we do at Breaking Par to help debug our LotusScript agents.
The first thing that we do is follow a set structure for all our LotusScript. If it's a function or subroutine (in a script library, as part of an agent, as part of a form, etc), then here is the structure:
Sub MySubName(myParam As String)
On Error Goto BubbleError
' This comment describes what the subroutine does
Dim localVariable As String
' // Insert subroutine code here
Error Err, Error$ & Chr$(10) & " in procedure " & Getthreadinfo(1) & ", line " & Cstr(Erl)
(Note that the parameter and the defined variable are only used as examples)
The first line of every function or subroutine tells LotusScript where the error should be trapped. This is done even before any variables are defined. The biggest reason for that is because each function/subroutine is going to have a different number of temporary variables. Since we want the error trapping to be in every function and subroutine no matter what, it's easy to find if it's at the top.
After the error trapping line comes a description of the function or subroutine. This can be one line or more, but we demand from our developers that it's in there. We like the code to be commented so it's easier to support.
Next comes the body of the function or subroutine. Then, at the bottom, is the error trapping. First, if the subroutine or function has terminated normally we exit out before getting to the error trapping code. If there has been an error, then that error is bubbled up to the calling procedure. The name of the error is passed along with the name of the current function or subroutine (that's what Getthreadinfo(1) does) and the line number in the current function or subroutine. Since we are using the keyword Error, then the calling function or subroutine will trigger an error, which will go into its error trapping code, which will add on the procedure name and its line number, and so on.
At some point the bubbling will need to stop. For an agent, this is in the Sub Initialize area. For forms, it will be the form event that triggered the code. In that part (where the code is initiated) we follow a slightly different structure:
On Error Goto BubbleErrorStop
' This comment describes what the agent does
Dim localVariable As String
' // Insert subroutine code here
Dim errMsg As String
Dim errSession As New NotesSession
Dim errDoc As NotesDocument
errMsg = Error$ & Chr$(10) & " in procedure " & Getthreadinfo(1) & ", line " & Cstr(Erl)
On Error Resume Next
Set errDoc = errSession.CurrentDatabase.CreateDocument
Call errDoc.ReplaceItemValue("Form", "Error")
Call errDoc.ReplaceItemValue("UserName", errSession.UserName)
Call errDoc.ReplaceItemValue("NotesVersion", errSession.NotesVersion)
Call errDoc.ReplaceItemValue("NotesBuildVersion", errSession.NotesBuildVersion)
Call errDoc.ReplaceItemValue("Platform", errSession.Platform)
Call errDoc.ReplaceItemValue("ErrorMessage", errMsg)
Call errDoc.Save(True, True, True)
errMsg = Left(errMsg, Instr(errMsg, Chr$(10))-1)
Msgbox errMsg & Chr$(10) & Chr$(10) & "Support has been notified of the error."
Err = 0
The flow is similar to what we had before - the error trapping line is the first line and the description comes next followed by the actual code. Before the exit on normal termination, we have a label of "AgentDone". This will be used for a resume location in the case of an error. We resume to this location, which terminates the agent.
Our error trapping code this time is a lot longer. We want to report the error. We do this by creating an "Error" document in the current database. Note that we make no assumptions about having a session or the current database or anything. This way, the error trapping code is generic and can be copied and pasted into the next agent, no matter who is coding the agent (they don't have to name their session variable a certain way, or anything). The only requirement is that they don't use those three variable names.
We report the full error message (along with all the bubbled-up procedure information). We also capture the user's platform and their Notes client information. This useful for tracking down problems with a particular version of Notes. That document is saved in the current database. (Notice that we ignore any error in the save - if the user only has reader access, their problem won't be reported in this setup. If there will be a lot of users with reader access, instead of saving the document it is mailed to the support person). We then strip out all the procedure information from the error. This is done because we are going to show the error message to the user and they don't care about all the steps it took to get there. So the error message is given to the user along with a "comforting" message that support has been notified.
Finally, we clear the error number just so the next LotusScript that runs in guaranteed to "start fresh" and we tell the agent to resume at the "AgentDone" tag, which exits the agent. If we didn't have that last line in there, Notes would put up a "No Resume" message to the user.
So that is our error trapping. It is similar to the techniques talked about in the articles linked above. Having the full thread information makes it so much easier to track down the source of the error message. When you have an agent that includes a few script libraries and you get a user who calls and says they got a "type mismatch" error running the agent, without this trapping you have to try the agent yourself and hope it causes the same error. With this trapping, you can look at the error document and see what line caused the error. Then you just go into the design, follow the "row number" value (lower right corner, the first of the two numbers) down to the line that caused the error, and you know exactly where the error happened.