Don't Fear Exceptions
By Thom McGrath on
When I was new to what was then known as REALbasic, I had come from interpreted environments such as Perl and PHP. PHP had no exceptions at all back then, and I never encountered them in Perl. So when I started encountering exceptions in REALbasic, it was a totally foreign concept. My gut reaction is to make them go away. While correct, making them go away is a good thing, there are many wrong ways to do it. The trick is to make exceptions work for you. They are very powerful tool, make them work for you, not against you.
Understanding how exceptions interact with your code begins with understanding how your code flows. A great way to see this in action is by writing an event-driven console app. By default, console apps have no event loop: they start, do their job, and exit. To make an event-driven console app, you need to insert your own event loop. When you do this, you're actually somewhat close to what a desktop app does for you.
Your basic event loop might start out looking like this:
While True App.DoEvents() Wend
And you're in business. This is a very simple loop though, your app will run forever, or until an unhandled exception occurs. So the next step is to handle some exceptions.
While True Try App.DoEvents() Catch ShouldQuit As EndException Exit Catch Err As RunTimeException Dim Handled As Boolean = UnhandledException(Err) If Not Handled Then DisplayExceptionMessage(Err) Exit End If End Try Wend
This code is not intended to compile, it merely illustrate a basic desktop app's event loop. This is what happens behind the scenes. When you call the global Quit method, it raises an EndException which is caught by the exception handler, and the infinite loop gets terminated. This is important because exceptions have a very powerful behavior: they prevent all code after the Raise call from firing until the exception has been dealt with. So having Quit fire an exception means the lines after the Quit method will never get executed.
The second part of this is hooking up the UnhandledException event. In this event, when you return true, you are signaling that you want execution to continue. If you return false, the loop will be terminated and your app quits.
Which brings us to what every developer fears: unhandled exceptions. In short, it means a mistake was made that you weren't prepared for. Let's get one thing out of the way first: an unhandled exception means your code did not execute as you intended. So how can you trust the rest of your app? Remember how you just read that exceptions prevent code from executing until handled? That means code you intended to have execute, did not get a chance. Your app is in an unpredictable state, and really needs to shut down. For this reason, I advise all Xojo developers: never return true in App.UnhandledException, unless you are calling Quit, in which case it doesn't matter what you return. App.UnhandledException should be used for logging error data, maybe sending it to a server somewhere, notifying the user of a problem, and shutting down. Returning true and ignoring the exception is just asking for more trouble. If you knew what you code was going to do after the exception was ignored, you probably would have handled the exception in the first place.
So great, now I've told you exceptions should bring your app down. Doesn't that sound worse? Until you start to handle your exceptions, yes, it is worse. You can either start sprinkling Exception blocks at the end of your methods, or you can use the Try block. My preference is the Try block, because it puts the error handling closer to the error, and allows you a little more control. Below is an example function that takes two strings which should contain SQL-formatted dates, and returns the date that is more recent.
Function MostRecent (Date1 As String, Date2 As String) As Date Dim FirstDate As New Date FirstDate.SQLDate = Date1 Dim SecondDate As New Date SecondDate.SQLDate = Date2 If FirstDate.TotalSeconds > SecondDate.TotalSeconds Then Return FirstDate Else Return SecondDate End If Exception Err As UnsupportedFormatException MsgBox "One of the supplied dates is not formatted correctly." Return Nil End Function
This alternate version uses Try/Catch to provide more error details.
Function MostRecent (Date1 As String, Date2 As String) As Date Dim FirstDate As New Date Dim SecondDate As New Date Try FirstDate.SQLDate = Date1 Catch Err As UnsupportedFormatException MsgBox "The first date, " + Date1 + ", is not formatted correctly." Return Nil End Try Try SecondDate.SQLDate = Date2 Catch Err As UnsupportedFormatException MsgBox "The second date, " + Date2 + ", is not formatted correctly." Return Nil End Try If FirstDate.TotalSeconds > SecondDate.TotalSeconds Then Return FirstDate Else Return SecondDate End If End Function
Both are technically correct, but my preference is the second form, since it can provide more detailed error information.
Now, let's make exceptions work for you. Consider a class whose constructor accepts a numeric value in a certain range, say 1-9. What should happen if the constructor is provided the value 10? Should it automatically limit the value to 9? Maybe ignore it and use a default? Maybe it simply displays a message box. In all cases though, you still have an instance of the class whose value doesn't match the intended value. Yes, the intended value may be wrong, but using a different value can just lead to more problems.
The only logical answer in this case is to fire off an Exception inside the constructor. A built-in exception, such as UnsupportedFormatException in this case, may work if it makes sense. Sometimes though, there is nothing built-in which makes sense. But since RuntimeException is a class, you can subclass it. You can make your own exceptions, and even add your own properties to them. All exceptions have a Message property, so you can provide more details about the problem too. In this example constructor, the code might look like:
Sub Constructor (Value As Integer) If Value >= 1 And Value <= 9 Then Self.mValue = Value Else Dim Err As New UnsupportedFormatException Err.Message = "Expected value between 1 and 9, got " + Str(Value,"-0") + "." Raise Err End If End Sub
In this case, the exception informs you (since your users should never see exception details, they don't care) exactly what the expected range is, and what value was received. And, since exceptions block execution until handled, the code calling the constructor will be forced to deal with the problem, rather than ignore it by default.
And that's really the problem exceptions allow you to solve: ignorable errors. If you want to ignore an exception, you have to write code to do so. If you want to ignore an ErrorCode property, you do nothing. It reverses the role: exceptions are implicitly handled, explicitly ignored. Error properties are implicitly ignored, explicitly handled. And since it is an error, shouldn't it be handled?
The Xojo database classes are a perfect example of where exceptions should have been used but were not. Consider the following code:
MyDatabase.SQLExecute("BEGIN TRANSACTION") MyDatabase.SQLExecute("INSERT INTO sometable (object_name) VALUE ('some object')") MyDatabase.SQLExecute("COMMT TRANSACTION")
Look closely at this code. The "I" in "COMMIT" is missing, causing an SQL error. The transaction will not be committed, meaning the value won't get added, and worse, the transaction will be left open. This poses a very serious problem, but this code will not give you any indication something is wrong unless you do something about it. So let's try again:
MyDatabase.SQLExecute("BEGIN TRANSACTION") If MyDatabase.Error Then MsgBox "SQL Error: " + MyDatabase.ErrorMessage End If MyDatabase.SQLExecute("INSERT INTO sometable (object_name) VALUE ('some object')") If MyDatabase.Error Then MsgBox "SQL Error: " + MyDatabase.ErrorMessage End If MyDatabase.SQLExecute("COMMT TRANSACTION") If MyDatabase.Error Then MsgBox "SQL Error: " + MyDatabase.ErrorMessage End If
Look at all that duplicate code. Plus, each line will still get executed. In this particular example, that isn't a big deal, but there are many scenarios where a single error in the transaction should cause a rollback. You should be able to see how this code just keeps getting more and more complex. Handling errors caused by Xojo database objects is an awful experience.
Now, let's assume that the database classes fired exceptions on error. The original code could have been written as such:
Try MyDatabase.SQLExecute("BEGIN TRANSACTION") Try MyDatabase.SQLExecute("INSERT INTO sometable (object_name) VALUE ('some object')") MyDatabase.SQLExecute("COMMT TRANSACTION") Catch Err As SQLException MsgBox "SQL Error: " + Err.Message + " in SQL " + Err.SQL MyDatabase.SQLExecute("ROLLBACK TRANSACTION") End Try Catch Err As SQLException MsgBox "Could not begin transaction" End Try
In this case, if the "BEGIN TRANSACTION" were to fail, nothing else happens. If it succeeds and an error occurs while inside the transaction, a "ROLLBACK TRANSACTION" is issued. (Note that in this code, an error from the "ROLLBACK TRANSACTION" would go unhandled.) But even without any error handling at all, the very first version would still alert the developer where the problem is. With the error handling, our imaginary SQLException object can provide us with both the message and which statement caused the error. And as an added benefit, if the "INSERT" were to fail, the "COMMIT" would never get executed at all.
One last tip: the BreakOnExceptions pragma. If there is an instance where you fully expect an exception, such as the Keychain class, use this pragma to prevent the debugger from interrupting:
#pragma BreakOnExceptions Off Try Return System.KeyChain.FindPassword(Item) Catch Err As KeyChainException Return "" End Try #pragma BreakOnExceptions Default
This code will return the password if found, or "" if not found, all without interrupting your debugging session.
Exceptions should not be feared. They are a powerful tool. Use them correctly, and your app can become much more stable. Embrace them.