Previous Home Contents Next

Chapter 31: Exceptions

31.1 : Introduction

What happens when something goes wrong during a call to a function? For example, the system detects an error in its arguments (eg. divide by zero), or that it can't perform a particular operation (eg. opening a file). Using normal call and return code, you have two options: The function can cause the program to halt, eg. by a call to ``error'', or it can return a Union type which includes a flag to say ``failed''. The former may be a bit extreme, especially if the program is in some way interactive. The latter option then causes every function that calls it to have to be aware of the failure case, which may or may not be useful. It certainly gets long-winded if the failure case has to be propogated through a deep call-chain. A more consise way of signalling error conditions is provided by the exceptions mechanism. There are four parts to the mechanism:

  1. Throwing (or raising) an exception
  2. Catching exceptions that we are interested in
  3. Declaring which exceptions may be thrown by an expression
  4. Defining exception objects

31.2 : Throwing Exceptions

The syntax for raising an error is:
   except exception
where exception is an expression evaluating to an exception object. When an exception is thrown, execution of the current function halts, and control is passed to the most recent handler (note that this uses dynamic scope for determining ``most recent''). The type of an except expression is Exit (but see later) --- ie. no value is created by the expression, but control does not pass to the expression (compare this with the return statement, for example). An except statement is therefore a way of causing a function to terminate abnormally.

31.3 : Catching Exceptions

To catch an exception the syntax is:
   try Expr but id in handler always stmts
where Expr is the expression we are protecting, id is an identifier, handler is an (optional) error handler and stmts are some (optional) statements to do any resource freeing on abnormal exits. When a handler is executed, the id is bound to whatever exception was raised. The handler block is then evaluated. Typically it will look something like:

try ... but E in {
        E has ZeroDivide(Integer) => ...
        E has BadReduction => ...
        E has FileException => ...
        true => except E;
        never;
}
Where each item on the right hand side of the has denotes an exception type. The last line is to ensure that the statement compiles successfully (some work needs to be done on the syntax here). Each branch of the handler block (the parts after =>) should evaluate to the same type as that of the expression protected by the handler. This is so that one can do:
n := try divide(a, x) but E in { E has ZeroDivide => 22; ... }
which will attempt the division, and if successful assign the result to n, otherwise if a division by zero exception is raised, then n will have the value 22. After the handler has been executed, the stmts in the always part of the try expression are evaluated. These are guarenteed to be evaluated even if the handler itself raises an exception. The typical reason to use an always block is to deallocate any resources the function may have allocated, for example:
f := open(file);
try doWonderfulThings(f, data) always close(f);
Note that the 'but id in ...' part is also optional providing an always part is supplied. This will ensure that the file is always closed regardless of what exceptions are thrown by doWonderfulThings.

31.4 : Specifying Exceptions

It is possible to declare what exceptions are thrown by a particular expression. This is done using the except keyword as an infix operator (the two uses are rarely confused). The operator takes two arguments, a base type and a (possible empty) comma-separated list of exception types. Typically, the except keyword is applied to the return types of functions to indicate which exceptions they can raise, for example:

      foo: (args) -> returns except(x1,x2,x3,...)
 
This indicates that foo may only raise exceptions of types x1,x2,x3 etc. The programs behaviour is undefined if other exceptions are raised. To indicate that a function raises no exceptions, the tuple should be empty. If there is no except clause on the return type, then the compiler assumes that any exception may be raised by the function. This does lead to some unsafe code --- for example:

  justDie(): Integer == except ZeroDivide;

  badIdea(): Integer except () == justDie();
Here badIdea indicates that it will not raise an exception, while justDie will always raise an error. The compiler may warn the user in this situation, but not at the moment. The compiler will however check if any exceptions are explicitly raised that do not satisy the functions signature, for example:
  foo(): Integer except ZeroDivide == {
	  except FileError;
  }
will not compile. This also works within try blocks:
  bar(): () except Ex1 == {
      try zzz() but E in {
	  E has ExA => except ZeroDivide; --- error
	  E has ExB => except Ex1; --- OK
	  true => except E --- error
	  never
      }
  }
The except qualifier on types works just like any other type constructor, and so you can use it as part of a type as normal:
foo(fn: Integer -> Integer except ()): () == ...
indicates that the argument to foo must be a function that never raises an exception. Naturally, there are only a few places where it makes sense to use these types. NB: I simplified things earlier when I said that the type of an except statement was Exit --- the actual type of except X is Exit except typeof(X). Where typeof(X) indicates the exact type of the exception expression. This indicates that flow of contol stops, but the exception X may be raised. Exactly what we wanted, thank gawd.

31.5 : Defining Exceptions

An exception definition is made up of two parts --- a category definition and a domain definition. The category definition provides a means to specify related exceptions (so that ZeroDivide may inherit from ArithmeticError for example), and the domain definition provides a mechanism for creating the exception. For example,

define ZeroDivide: Category == ArithmeticError with;
ZeroDivide: ZeroDivide@Category == add;
OK. so this is ugly. Write a macro to tidy it up. If ZeroDivide is defined this way, then any handler with a clause E has ArithmeticError will also catch ZeroDivide exceptions. There is an is keyword in the language if you need to do exact matching, but it shouldn't be needed that often. This mechanism also allows one to create parameterised exceptions:
define ZeroDivide(R: Ring): Category == ArithmeticError with;
ZeroDivide(R: Ring): ZeroDivide(R)@Category == add;
and to have values defined within the exception:
define FileException: Category == ArithmeticError with {
        name(): String;
}

FileException(vvv: String): FileException@Category == add { 
        name(): String == vvv;
}
In fact, there is a myriad of ways to insert values into exceptions:
foo(): () == {
        ...
        except (add {name(): String == })_
		@FileException
        ...
}
is one, which has the single advantage that name will only be calculated if it is actually used. Defining a 'lazy' version of FileException is a much better thing to do in this situation. Another example that keeps the add definition simple, but sort of loses it with respect to the number of objects defined.
define FileException: Category == Exception with {
        name(): String;
}

define FileException(vvv: String): Category == 
	FileException@Category with {
        default name(): String == vvv;
}

FileException(vvv: String): FileException(vvv)@Category == add;


Previous Home Contents Next