This post offers an introduction to the usefulness of exception handling in general, while also explaining how to deal with a particular shortcoming when you use debugging code and the UnhandledException event handler in built programs you deliver to people. I hope this article will be helpful to both newcomers and veterans alike.
Background (UnhandledException and the Stack Trace)
You probably know the UnhandledException event handler in the App class.
It lets you catch any unhandled exceptions in your program, preventing your program from getting terminated unexpectedly, and allowing you to save any interim data that the user may have not saved yet, and other defensive fallback procedures.
One other use of this handler is to record the location where the exception occurred in order to use that for later debugging. For instance, if you send your built application to others who do not have the Xojo debugger, your program could still record the so-called stack trace from the exception, show that to the user, and ask him to forward that information to you so you can hopefully deduce what went wrong and how to avoid it in the future.
You get this stack trace by invoking the Stack() method of the RuntimeException object you get passed in UnhandledException. It returns an array of strings, identifiying the methods that were called up to the one that caused the exception.
For instance, if your program causes an exception in a Window's Open event, the stack trace might look like this:
1. RaiseOutOfBoundsException
2. Window1.Window1.Event_Open%%o<Window1.Window1>
3. FireWindowOpenEvents
4. Window.Constructor%%o<Window>
5. Window1.Window1%o<Window1.Window1>%
6. _MakeDefaultView
7. _Startup
8. RunFrameworkInitialization
9. REALbasic._RunFrameworkInitialization%%p
10. _Main
11. % main
The topmost line shows where the exception occured, the bottommost is the topmost caller at that time.
It tells several things:
- The exception, obviously an OutOfBounds exception, occured in Window1's Open event (line #2)
- The Open event was invoked by the Window1's Constructor (lines 4 and 5).
- The Window1 got constructed right at start of the program (line 7). Otherwise, we'd usually see the method "RuntimeRun" up in the stack trace, indicating that the startup has finished and the program is now processing events.
Raising exceptions
Whenever an exception occurs, we say that it gets raised. Xojo even offers a custom statement for that: The Raise command. With that, you can make your own exceptions occur. An example:
raise new OutOfBoundsException
is equivalent to:
dim anArray(0) as Integer
anArray(1) = 0 // this will cause an OutOfBoundsException
(Of course, you can create your very own exception types by subclassing them from RuntimeException. See the Language Reference to learn more about that.)
Now, here's an imporant part: The stack trace that gets attached to any RuntimeException object is created when this raise statement is invoked. And the stack trace is then created from the very calling stack that's in effect at the time of invocation. Logical, right?
Catching exceptions
You can catch exceptions in your methods using the try ... catch ... end and the exception statements.
There are two common cases why and how you'd catch exceptions:
1. Handling the exception
You expect a particular exception to occur and are prepared to handle it. This is, for instance, the case when you use file I/O operations and which you cannot otherwise predict to avoid them, such as that a file cannot be created, causing an IOException.
2. Cleanup
You do not know that any particular exceptions might occur in some subroutines you call but you like to be prepared and do some cleanup if that should happen. In this case, you'll pass on the exception to the caller because you don't know have reason way to handle the exception at that particular point.
Such cleanup handling code usually looks like this:
try
... // your operations
catch exc as RuntimeException
db.Rollback // e.g. abort a database transaction
raise exc // pass the exception on
end
The complication (catching and re-raising)
Let's assume you have a UnhandledException event handler in the App class, which reads the Stack array and for later analysis (e.g. by writing to a file, creating an email to you or even uploading it to your web server).
So, if any part of your program raised an exception for which you don't have particular handling code, it'll end up in App.UnhandledException, which will eventually allow you to learn where the crash ocurred. That's the general idea, at least.
However:
Consider what happens if an exception occurs inside a subroutine that's called by code using the Cleanup exception handler?
Remember how the stack trace gets created: During the invocation of the raise statement.
This means that if an exception occurs deeper down, then gets caught by the Cleanup handler code, which then re-raises the exception to end up finally in the App.UnhandledException handler, you'll get to see the wrong Stack trace. You won't see the methods that were actually the cause of the exception but only the stack trace from the upmost raise call.
The solution
Finally, we're getting to the root of this article's purpose.
I'm suggesting that whenever you have a raise exc call in your code, you change that to:
raise new CaughtException (exc)
And, of course, you'll need a new class CaughtException which I'll show you here:
First, create a new Class, name it CaughtException and set its Super class to RuntimeException.
Then add the following two methods to it:
Sub Constructor(exc as RuntimeException)
if exc isA CaughtException then
exc = CaughtException(exc).OrigExc
end
self.OrigExc = exc
End Sub
Function Stack() As String()
return self.OrigExc.Stack
End Function
Finally, add this private property:
OrigExc As RuntimeException
That's all you need to care about.
The trick here is that this class overwrites the Stack() method of RuntimeException, providing the original stack trace instead of the (useless) one created by the raise command. Therefore, your App.UnhandledException handler won't have to be changed at all to make this work.
Even if you do not fully comprehend what this is doing, simply follow my advice of always using this construct wherever you catch an exception and raise it right after again. It'll magically make your debugging efforts easier in the future.