Friday, July 13, 2012

Preserving stack trace when catching and re-raising runtime exceptions

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 Real Studio 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 a 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. Realbasic 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.

1 comment:

Eric de La Rochette said...

That's a very smart trick. But what if you're using the .Message or .ErrorNumber properties in the UnhandledException event? You'll need to access the ones that pertain to the original exception.

Given that they properties, is it reasonable to add the following Methods to the CaughtException class:

Function Message() As String
return self.OrigExc.Message
End Function

Function ErrorNumber() As Integer
return self.OrigExc.ErrorNumber
End Function

Or would it be better to have a OriginalException method that simply return Self.OrigExc?

--
Eric.