Exceptions

TADS 3 provides an "exception" mechanism, which allows the program to signal and handle unusual conditions in a structured manner.  TADS 3 exceptions work very much like Java exceptions, so if you've used Java you'll find the TADS 3 exception mechanism familiar.

 

An exception is any unusual condition.  Exceptions usually indicate errors, but they don't have to; they can also be used for a variety of other purposes, such as terminating a procedure early or recovering from a resource shortage.  An exception is represented by an object; you can tell what kind of exception you have by looking at the class of the exception object.  Exception objects are the same as any other objects, so you can create as many different types of exception classes as you need.

 

Exception handling has two components: "throwing" and "catching."

 

When something unusual occurs in your program that you wish to handle using the exception mechanism, you "throw an exception."  This means that you create a new object to describe the exception, then use the throw statement (new to TADS 3) to throw the exception.  The throw statement is a little like return or goto, in that it abruptly transfers execution somewhere else; hence any statement immediately following a throw is unreachable (unless, of course, it can be reached by a label or some other means that doesn't involve going through the throw statement).


Where does throw send execution?  This is where catching comes in.  When you throw an exception, the VM looks for an enclosing block of code protected within a try block.  This search is done according to the "call chain" that the program has executed to reach the current location – in other words, the series of function and method calls that have been made so far.  When the VM finds an enclosing try statement, it looks at the associated catch blocks.  If there's a catch for a superclass of the thrown exception, the VM transfers control to the code within the catch block; otherwise, the VM skips that try and tries to find an enclosing one.

 

For each try statement that encloses the current code but doesn't define a catch block for the thrown exception, the VM checks to see if the try has an associated finally block, and executes the enclosed code before looking for an enclosing try block.

 

If there's no enclosing try block with a catch for the thrown exception, the VM terminates execution of the program.  The VM will check the exceptionMessage property of the unhandled exception object, and display the value of the property if the value is a (single-quoted) string value.

 

A try statement looks like this:

 

   try
   {
      // some code that might throw an exception, or call
      // a function or method that might do so
   }
   catch (FirstExceptionClass exc1)
   {
      // handle FirstExceptionClass exceptions
      // exc1 is a local with the thrown exception object
   }
   catch (SecondExceptionClass exc2)
   {
      // handle SecondExceptionClass exceptions
      // exc2 is a local with the thrown exception object
   }
   finally
   {
      // do some cleanup work – this gets called
      // whether an exception occurs or not
   }

 

A try statement can have as many catch clauses as needed – it can even have no catch clauses at all.  The finally clause is optional, but only one is allowed if it's present at all, and it must follow all of the catch clauses.

 

Each catch clause has a name following the name of the exception.  The catch creates a new local variable with the given name – the variable is local to the code within the catch clause.  When the exception is caught, the VM will store a reference to the thrown exception object in this variable; this is the same object that was used in the throw statement.

 

The VM searches for a catch clause that matches the exception class starting with the first catch associated with the try, and considers each catch in turn until it finds a match.  A catch matches if the named class is a superclass of the exception behing handled.  Because the catch clauses are tried in order, you can have one handler for a specific type of exception, and also have a later handler for a superclass of the first exception; the specific type will be handled by the first handler, since the VM will find that handler earlier than the more general handler.  In such cases, only the first matching handler will be invoked.

 

If a finally clause is present, the VM will always execute the code contained within, no matter how control leaves the try block.  If control leaves via an exception that isn't handled by any of the try statement's catch clauses, the VM will execute the finally code before it continues the search for the next enclosing try.  If no exceptions occur, so control leaves the try block normally, the finally code is executed immediately after the last statement in the try block.  If an exception is thrown but one of the try statement's catch clauses catches the exception, the VM executes the finally code immediately after the last statement in the catch block.

 

Note that the code in a finally clause will execute no matter how execution leaves the try block.  This even includes goto, return, break, and continue statements.  If the try block contains a return statement, the program will first calculate the value of the expression being returned (if any), then it will execute the finally code, and only then will control transfer back to the caller of the current function or method.  (It's important that the return value is calculated first, because it counts as code that's protected by the try.  If an exception is thrown while calculating that value, it'll be handled the same as any other exception thrown inside the try.)   If you use goto, break, or continue within the try block to jump to a statement that's outside the try block, the program will execute the finally code just before jumping to the target statement.

 

Here's an example that illustrates how all of this works.

 

#include "tads.h"

#include "t3.h"

 

_main(args)

{

    t3SetSay(_say_embed);

    a();

}

 

_say_embed(str) { tadsSay(str); }

 

class Exception: object

    construct(msg) { exceptionMessage = msg; }

    exceptionMessage = ''

;

 

class ResourceError: Exception;

class ParsingError: Exception;

 

 

a()

{

    b(1);

    b(2);

    b(3);

}

 

b(x)

{

    "This is b(<<x>>)\n";

   

    try

    {

        c(x);

    }

    catch (Exception exc)

    {

        "b: Caught an exception: <<exc.exceptionMessage>>\n";

    }

   

    "Done with b(<<x>>)\n";

}

 

c(x)

{

    "This is c(<<x>>)\n";

   

    try

    {

        d(x);

    }

    catch(ParsingError perr)

    {

        "c: Caught a parsing error: <<perr.exceptionMessage>>\n";

    }

    finally

    {

        "In c's finally clause\n";

    }

   

    "Done with c(<<x>>)\n";

}

 

d(x)

{

    "This is d(<<x>>)\n";

    e(x);

    "Done with d(<<x>>)\n";

}

 

e(x)

{

    "This is e(<<x>>)\n";

 

    if (x == 1)

    {

        "Throwing resource error...\n";

        throw new ResourceError('some resource error');

    }

    else if (x == 2)

    {

        "Throwing parsing error...\n";

        throw new ParsingError('some parsing error');

    }

   

    "Done with e(<<x>>)\n";

}

 

When this program is run, it will show the following output:

 

This is b(1)

This is c(1)

This is d(1)

This is e(1)

Throwing resource error...

In c's finally clause

b:  Caught an exception:  some resource error

Done with b(1)

This is b(2)

This is c(2)

This is d(2)

This is e(2)

Throwing parsing error...

c:  Caught a parsing error:  some parsing error

In c's finally clause

Done with c(2)

Done with b(2)

This is b(3)

This is c(3)

This is d(3)

This is e(3)

Done with e(3)

Done with d(3)

In c's finally clause

Done with c(3)

Done with b(3)

 

This illustrates several aspects of exceptions.

 

First, note that function d() doesn't have any exception handlers (i.e., it has no try block).  Since this function is not concerned with catching any exceptions that occur within itself or functions it calls, it doesn't need any exception handlers.  This is one of the advantages of exceptions over using return codes to indicate errors: intermediate routines that don't care about exceptions don't need to include any code to check for them.  When searching for a try block, the VM simply skips function d() if it's in the call chain, since it has no handlers.

 

Second, note that function c() only handles ParsingError exceptions.  Since this function has no handlers for any other exception types, the VM skips past this function when trying to find a handler for the ResourceError exception.  So, not only can a function ignore exceptions entirely, but it can selectively include handlers only for the specific exceptions it wants to handle, and ignore anything else.


Third, note that, once an exception is caught, it no longer disrupts the program's control flow.  In other words, an exception isn't re-thrown after it's caught, unless you explicitly throw it again with another throw statement in the handler that caught it.

Run-time Error Handling

When your program does something illegal, such as trying to multiple two strings together or extracting an element from a list at an invalid index, a run-time error occurs.  In past versions of TADS, there wasn't much you could do about such errors; the interpreter displayed a message and ended the current command.  TADS 3, in contrast, treats all run-time errors as ordinary exceptions; this means that you can handle run-time errors using the same try/catch mechanism that you use to handle your own exceptions.

 

The compiler looks for an object class called RuntimeError defined in your program.  If this class is defined, the compiler will set up the image file so that the VM creates an object of this class whenever a run-time error occurs.

 

The RuntimeError class has one special requirement, which is that it must provide a constructor with this interface:

 

    construct(errno, ...)

 

The first argument is the VM error code; any additional arguments are parameters to the error.  In addition, the VM will automatically set the exceptionMessage property to a (single-quoted) string giving a descriptive message for the run-time error.