Writing safer code in REALbasic

Thanks to Ryo Amano, there's also a Japanese version of this text available.

Index

Introduction

Just this morning (actually, still laying in bed in that kind of not-dreaming-anymore mode), it appeared to me how easy and nicely you can do exception handling with RB (I knew already its basics, but the realization that an Exception can be extended because of its nature of being a class, struck me like lightning and made me jump out of the bed right away and turning me Mac on to write this down for all of you):

Exception (or error) handling is hardly documented in RB; the docs only give you some short explanation of its syntax, but no real-world examples or other ideas for what it is good for.

OK, so let's make this one a little tutorial for all of you RB users that are kind of new to Object oriented and other modern programming techniques.

BTW, here's my little copyright notice: Redistribution of this is permitted as long as it is done for free. I retain the copyright. Thomas Tempelmann ( ).

 

What's the purpose of Exception Handling?

Exception handling comes in handy generally any time you want to abort a process in a procedure (subroutine) because of some error condition.

Consider you write a procedure that adds a line of text plus a number, that always is supposed to be positive, to a file. The basic code would look like this:

  Sub AddLineToTextfile(text as String, posNum as Integer, fileRef as FolderItem)
    Dim fileStream As TextOutputStream
    
    fileStream = fileRef.AppendToTextFile
    fileStream.WriteLine (text + " (the number is: " + Str(posNum) + ")")
    fileStream.Close
  End Sub

The code using that subroutine could be like this:

  Sub WriteSomeLinesToAFile()
    Dim tmpFileRef as FolderItem
    tmpFileRef = GetFolderItem("tmpFile.txt")
    
    if tmpFileRef.Exists then
      tmpFileRef.Delete()
    end
    
    AddLineToTextfile("2*2", 2*2, tmpFileRef)
    AddLineToTextfile("5*5", 5*5, tmpFileRef)
  End Sub

The problem is that the AddLineToTextfile routine can not always be sure that its caller has set up everything correctly:

There's several conditions that the caller must meet:

As long as you know these conditions, you can make sure that you meet them before you call the subroutine.

For instance, if you later want to re-use the AddLineToTextfile routine in other projects, or even publish it to the public, you want to make sure in advance that your subroutine is called properly.

How can you make sure that these conditions are met?

Raising Exceptions

When you detect an error condition in your subroutine, and you want to handle it, the classic way would look like this:

  // make sure that the passed number is positive:
  IF posNum < 0 THEN
    // oops - error!
    RETURN
  END
  IF fileRef = NIL THEN
    // oops - another error!
    RETURN
  END
  // ... go on with the normal process

While that takes care of making your code fail-proof, it does not tell the caller that he didn't meet your conditions.

You could either handle that by returning some value that tells the caller whether he did everything right or not. However, that requires that the caller not only adds code to retrieve that additional return value, but that he also checks the value and handles it accordingly.

Or, you could simply signal the error condition by using the MsgBox() procedure like this:

  IF posNum < 0 THEN
    MsgBox "error in AddLineToTextfile: posNum < 0!"
    RETURN
  END

That, however, is not recommended, because

Raising an Exception solved all these problems. Here's how it would look like in your code:

  IF posNum < 0 THEN
    Raise new RuntimeException
  END
  IF fileRef = NIL THEN
    Raise new RuntimeException
  END

If you raise an Exception in your code, it works like this: First, it acts as if you had used the RETURN statement instead: The procedure will be exited. And here comes the smart part: If the calling procedure does not explicitly handle the exception, it will not get forgotten (as in the case when you had returned a success value), but will get passed on to its caller, and then to that one's caller, and so on, until finally a caller handles the exception. However, if no one handles the exception explicitly, RB will invoke a standard error dialog that stops execution of your application and shows you the error condition in the editor.

Handling Exceptions

So, to handle the exception, you'll have to add a so-called exception handler to the end of your procedure:

  Sub WriteSomeLinesToAFile()
    ...
  Exception exc as RuntimeException
    MsgBox "oops - there's something wrong"
  End Sub

Catching exceptions as seen above is the most generic way of catching any exceptions in your application, including cases of using a nonexisting Object (happens when an object reference is NIL), a stack overflow (happens when subroutines are too deeply nested, especially in recursive calling), and array indexing errors (when the addressed array element is out of bounds).

When you add such code to all of your your event handler routines, you will be able to trap any error conditions in your application and gracefully handle them instead of having your application automatically quit each time such a exception occurs.

Creating your own Exception types

Raising a "RuntimeException" is a generic and quite unspecific message to the caller. It would be nicer if the subroutine can raise its exceptions with some more information, like a text message explaining the error condition in more detail or even giving more information about the parameters that are bound to the condition (like the file name or the value of the non-positive number).

For that, we have to create a new subclass of a "RuntimeException" class: In the RB editor, create a new class, name it "MySpecialException" and set its Super class to RuntimeException.

Now, instead of raising a RuntimeException, we can raise our own one:

  IF posNum < 0 THEN
    Raise new MySpecialException
  END

In an exception handler, we can make use of this:

  Exception exc as RuntimeException
    IF exc isA MySpecialException THEN
      MsgBox "oops - AddLineToTextfile failed"
    ELSE
      MsgBox "oops - there's something else wrong"
    END
  End Sub

Now, since we do not want to handle those unknown exceptions in our WriteSomeLinesToAFile procedure, but only the exceptions that we are aware of (those explicitly raised in AddLineToTextfile), we can pass the other exceptions up to higher levels in the calling stack by raising them again:

  Exception exc as RuntimeException
    IF exc isA MySpecialException THEN
      MsgBox "oops - AddLineToTextfile failed"
    ELSE
      // some other exception - we don't handle that here
      Raise exc
    END
  End Sub

As a short form, the above can be written like this:

  Exception exc as MySpecialException
    MsgBox "oops - AddLineToTextfile failed"
  End Sub

(this means that only exceptions of type MySpecialException are handled here, all others are passed on to higher levels.)

Adding more information to Exceptions you raise

Thanks to the concepts of Object Oriented programming and to the fact that an Exception is an object, too, we can benefit from this by making our exception signalling even smarter and quite universal:

In the RB editor, let's add a property to the MySpecialException class.

  Add the property: msg as String

Now, when we raise an exception of that type, we can add some information in its msg property, like this:

  Dim myExc as MySpecialException 
  ...
  IF fileRef = nil THEN
    myExc = new MySpecialException
    myExc.msg = "fileRef = nil (posNum = " + Str(posNum) + ")"
    Raise myExc
  END
  IF posNum < 0 THEN
    myExc = new MySpecialException
    myExc.msg = "file " + fileRef.name + ": posNum < 0"
    Raise myExc
  END

The exception handler then can use that message:

  Exception exc as MySpecialException
    IF exc.msg = "" THEN
      MsgBox "oops - AddLineToTextfile failed for unknown reason."
    ELSE
      MsgBox "oops - AddLineToTextfile failed: " + exc.msg
    END
  End Sub

Making your own subroutines safer

There's another problem you can handle nicely with an exception handler: In a subroutine of yours, there can be many error conditions that you aren't even aware of and thus cannot explicitly handle and prevent them there.

However, what you can at least do is to take care of those exceptional cases and do a "cleanup" of the data you affected in that subroutine.

For instance, if you open and write to a file in a subroutine, and then some unpredictable error, like a stack overflow, happens in there, you should at least make sure that you close the file before you lose its reference.

Here's an example:

  Sub AppendSomeDataToFile (fileRef as FolderItem)
    Dim fileStream As TextOutputStream
    Dim needsClose as Boolean
    fileStream = fileRef.AppendToTextFile
    needsClose = true
    fileStream.WriteLine ("just some data")
    needsClose = false
    fileStream.Close
  Exception exc as RuntimeException
    ' clean up in case of any error - in this case, close the file if it is still open
    IF needsClose THEN
      fileStream.Close
    END
    ' now pass on the exception because we're not able to handle it here
    Raise exc
  End Sub

These were the basics for handling exceptions in your code.

More suggestions and notes about effective exception handling

Final words

Thanks to Anjo Krank for his constructive comments and to Robert Ameeti, who pointed out some spelling and phrasing errors.

That's it for now. Please let me know if you have additions, corrections or questions to this text. Write to:


Back to Thomas' REALbasic page

 

Written and copyrighted March 14, 99 by Thomas Tempelmann; last edit: July 12, 2000