A Guide to the S-Lang Language: Error Handling

Next Previous Contents

## 15. Error Handling

All non-trivial programs or scripts must be deal with the possibility of run-time errors. In fact, one sign of a seasoned programmer is that such a person pays particular attention to error handling. This chapter presents some techniques for handling errors using S-Lang. First the traditional method of using return values to indicate errors will be discussed. Then attention will turn to S-Lang's more powerful exception handling mechanisms.

## 15.1 Traditional Error Handling

The simplist and perhaps most common mechanism for signalling a failure or error in a function is for the function to return an error code, e.g.,

> `

>         define write_to_file (file, str)
>         {
>            variable fp = fopen (file, "w");
>            if (fp == NULL)
>              return -1;
>            if (-1 == fputs (str, fp))
>              return -1;
>            if (-1 == fclose (fp))
>              return -1;
>            return 0;
>         }

> `

Here, the `write_to_file` function returns 0 if successful, or -1 upon failure. It is up to the calling routine to check the return value of `write_to_file` and act accordingly. For instance:

> `

>          if (-1 == write_to_file ("/tmp/foo", "bar"))
>            {
>               () = fprintf (stderr, "Write failed\n");
>               exit (1);
>            }

> `

The main advantage of this technique is in its simplicity. The weakness in this approach is that the return value must be checked for every function that returns information in this way. A more subtle problem is that even minor changes to large programs can become unwieldy. To illustrate the latter aspect, consider the following function which is supposed to be so simple that it cannot fail:

> `

>          define simple_function ()
>          {
>              do_something_simple ();
>              more_simple_stuff ();
>          }

> `

Since the functions called by `simple_function` are not supposed to fail, `simple_function` itself cannot fail and there is no return value for its callers to check:

> `

>          define simple ()
>          {
>              simple_function ();
>              another_simple_function ();
>          }

> `

Now suppose that the function `do_something_simple` is changed in some way that could cause it to fail from time to time. Such a change could be the result of a bug-fix or some feature enhancement. In the traditional error handling approach, the function would need to be modified to return an error code. That error code would have to be checked by the calling routine `simple_function` and as a result, it can now fail and must return an error code. The obvious effect is that a tiny change in one function can be felt up the entire call chain. While making the appropriate changes for a small program can be a trivial task, for a large program this could be a major undertaking opening the possibility of introducing additional errors along the way. In a nutshell, this is a code maintainence issue. For this reason, a veteran programmer using this approach to error handling will consider such possibilities from the outset and allow for error codes the first time regardless of whether the functions can fail or not, e.g.,

> `

>          define simple_function ()
>          {
>              if (-1 == do_something_simple ())
>                return -1;
>              if (-1 == more_simple_stuff ())
>                return -1;
>              return 0;
>          }
>          define simple ()
>          {
>              if (-1 == simple_function ())
>                return -1;
>              if (-1 == another_simple_function ())
>                return -1;
>              return 0;
>          }

> `

Although latter code containing explicit checks for failure is more robust and more easily maintainable than the former, it is also less readable. Moreover, since return values are now checked the code will execute somewhat slower than the code that lacks such checks. There is also no clean separation of the error handling code from the other code. This can make it difficult to maintain if the error handling semantics of a function change. The next section discusses another approach to error handling that tries to address these issues.

## 15.2 Error Handling through Exceptions

This section describes S-Lang's exception model. The idea is that when a function encounters an error, instead of returning an error code, it simply gives up and throws an exception. This idea will be fleshed out in what follows.

### Introduction to Exceptions

Consider the `write_to_file` function used in the previous section but adapted to throw an exception:

> `

>         define write_to_file (file, str)
>         {
>            variable fp = fopen (file, "w");
>            if (fp == NULL)
>              throw OpenError;
>            if (-1 == fputs (str, fp))
>              throw WriteError;
>            if (-1 == fclose (fp))
>              throw WriteError;
>         }

> `

Here the `throw` statement has been used to generate the appropriate exception, which in this case is either an `OpenError` exception or a `WriteError` exception. Since the function now returns nothing (no error code), it may be called as

> `

>          write_to_file ("/tmp/foo", "bar");
>          next_statement;

> `

As long as the `write_to_file` function encounters no errors, control passes from `write_to_file` to `next_statement`.

Now consider what happens when the function encounters an error. For concreteness assume that the `fopen` function failed causing `write_to_file` to throw the `OpenError` exception. The `write_to_file` function will stop execution after executing the `throw` statement and return to its caller. Since no provision has been made to handle the exception, `next_statement` will not execute and control will pass to the previous caller on the call stack. This process will continue until the exception is either handled or until control reaches the top-level at which point the interpreter will terminate. This process is known as unwinding of the call stack.

An simple exception handler may be created through the use of a try-catch statement, such as

> `

>          try
>           {
>             write_to_file ("/tmp/foo", "bar");
>           }
>          catch OpenError:
>           {
>              message ("*** Warning: failed to open /tmp/foo.");
>           }
>          next_statement;

> `

The above code works as follows: First the statement (or statements) inside the try-block are executed. As long as no exception occurs, once they have executed, control will pass on to `next_statement`, skipping the catch statement(s).

If an exception occurs while executing the statements in the try-block, any remaining statements in the block will be skipped and control will pass to the ``catch'' portion of the exception handler. This may consist of one or more `catch` statements and an optional finally statement. Each `catch` statement specifies a list of exceptions it will handle as well as the code that is to be excecuted when a matching exception is caught. If a matching `catch` statement is found for the exception, the exception will be cleared and the code associated with the catch statement will get executed. Control will then pass to `next_statement` (or first to the code in an optional `finally` block).

Catch-statements are tested against the exception in the order that they appear. Once a matching `catch` statement is found, the search will terminate. If no matching `catch`-statement is found, an optional `finally` block will be processed, and the call-stack will continue to unwind until either a matching exception handler is found or the interpreter terminates.

In the above example, an exception handler was established for the `OpenError` exception. The error handling code for this exception will cause a warning message to be displayed. Execution will resume at `next_statement`.

Now suppose that `write_to_file` successfully opened the file, but that for some reason, e.g., a full disk, the actual write operation failed. In such a case, `write_to_file` will throw a `WriteError` exception passing control to the caller. The file will remain on the disk but not fully written. An exception handler can be added for `WriteError` that removes the file:

> `

>          try
>           {
>             write_to_file ("/tmp/foo", "bar");
>           }
>          catch OpenError:
>           {
>              message ("*** Warning: failed to open /tmp/foo.");
>           }
>          catch WriteError:
>           {
>              () = remove ("/tmp/foo");
>              message ("*** Warning: failed to write to /tmp/foo");
>           }
>          next_statement;

> `

Here the exception handler for `WriteError` uses the `remove` intrinsic function to delete the file and then issues a warning message. Note that the `remove` intrinsic uses the traditional error handling mechanism--- in the above example its return status has been discarded.

Above it was assumed that failure to write to the file was not critical allowing a warning message to suffice upon failure. Now suppose that it is important for the file to be written but that it is still desirable for the file to be removed upon failure. In this scenario, `next_statement` should not get executed upon failure. This can be achieved as follows:

> `

>          try
>           {
>             write_to_file ("/tmp/foo", "bar");
>           }
>          catch WriteError:
>           {
>              () = remove ("/tmp/foo");
>              throw WriteError;
>           }
>          next_statement;

> `

Here the exception handler for `WriteError` removes the file and then re-throws the exception.

### Obtaining information about the exception

When an exception is generated, an exception object is thrown. The object is a structure containing the following fields:

error

  The exception error code (`Int_Type`).

descr

  A brief description of the error (`String_Type`).

file

  The filename containing the code that generated the exception (`String_Type`).

line

  The line number where the exception was thrown (`Int_Type`).

function

  The name of the currently executing function, or `NULL` if at top-level (`String_Type`).

message

  A text message that may provide more information about the exception (`String_Type`).

object

  A user-defined object.

If it is desired to have information about the exception, then an alternative form of the `try` statement must be used:

> `

>          try (e)
>          {
>             % try-block code
>          }
>          catch SomeException: { code ... }

> `

If an exception occurs while executing the code in the try-block, then the variable `e` will be assigned the value of the exception object. As a simple example, suppose that the file `foo.sl` consists of:

> `

>          define invert_x (x)
>          {
>             if (x == 0)
>               throw DivideByZeroError;
>             return 1/x;
>          }

> `

and that the code is called using

> `

>          try (e)
>          {
>             y = invert_x (0);
>          }
>          catch DivideByZeroError:
>          {
>             vmessage ("Caught %s, generated by %s:%d\n",
>                       e.descr, e.file, e.line);
>             vmessage ("message: %s\nobject: %S\n",
>                       e.message, e.object);
>             y = 0;
>          }

> `

When this code is executed, it will generate the message:

> `

>          Caught Divide by Zero, generated by foo.sl:5
>          message: Divide by Zero
>          object: NULL

> `

In this case, the value of the `message` field was assigned a default value. The reason that the `object` field is `NULL` is that no object was specified when the exception was generated. In order to throw an object, a more complex form of `throw` statement must be used:

> ``throw` exception-name [, message [, object ] ]`

where the square brackets indicate optional parameters

To illustrate this form, suppose that `invert_x` is modified to accept an array object:

> `

>         private define invert_x(x)
>         {
>            variable i = where (x == 0);
>            if (length (i))
>              throw DivideByZeroError,
>                    "Array contains elements that are zero", i;
>            return 1/x;
>         }

> `

In this case, the message field of the exception object will contain the string `"Array contains elements that are zero"` and the object field will be set to the indices of the zero elements.

### The finally block

The full form of the try-catch statement obeys the following syntax:

> `try [(opt-e)] { try-block-statements } catch Exception-List-1: { catch-block-1-statements } . . catch Exception-List-N: { catch-block-N-statements } [ finally { finally-block-statements } ]`

Here an exception-list is simply a list of exceptions such as:

> `

>         catch OSError, RunTimeError:

> `

The last clause of a try-statement is the finally-block, which is optional and is introduced using the `finally` keyword. If the try-statement contains no catch-clauses, then it must specify a finally-clause, otherwise a syntax error will result.

If the finally-clause is present, then its corresponding statements will be executed regardless of whether an exception occurs. If an exception occurs while executing the statements in the try-block, then the finally-block will execute after the code in any of the catch-blocks. The finally-clause is useful for freeing any resources (file handles, etc) allocated by the try-block regardless of whether an exception has occurred.

### Creating new exceptions: the Exception Hierarchy

The following table gives the class hierarchy for the built-in exceptions.

> `

>        AnyError
>           OSError
>              MallocError
>              ImportError
>           ParseError
>              SyntaxError
>              DuplicateDefinitionError
>              UndefinedNameError
>           RunTimeError
>              InvalidParmError
>              TypeMismatchError
>              UserBreakError
>              StackError
>                 StackOverflowError
>                 StackUnderflowError
>              ReadOnlyError
>              VariableUnitializedError
>              NumArgsError
>              IndexError
>              UsageError
>              ApplicationError
>              InternalError
>              NotImplementedError
>              LimitExceededError
>              MathError
>                 DivideByZeroError
>                 ArithOverflowError
>                 ArithUnderflowError
>                 DomainError
>              IOError
>                 WriteError
>                 ReadError
>                 OpenError
>              DataError
>              UnicodeError
>              InvalidUTF8Error
>              UnknownError

> `

The above table shows that the root class of all exceptions is `AnyError`. This means that a catch block for `AnyError` will catch any exception. The `OSError`, `ParseError`, and `RunTimeError` exceptions are subclasses of the `AnyError` class. Subclasses of `OSError` include `MallocError`, and `ImportError`. Hence a handler for the `OSError` exception will catch `MallocError` but not `ParseError` since the latter is not a subclass of `OSError`.

The user may extend this tree with new exceptions using the `new_exception` function. This function takes three arguments:

> `new_exception (exception-name, baseclass, description);`

The exception-name is the name of the exception, baseclass represents the node in the exception hierarchy where it is to be placed, and description is a string that provides a brief description of the exception.

For example, suppose that you are writing some code that processes numbers stored in a binary format. In particular, assume that the format specifies that data be stored in a specific byte-order, e.g., in big-endian form. Then it might be useful to extend the `DataError` exception with `EndianError`. This is easily accomplished via

> `

>        new_exception ("EndianError", DataError, "Invalid byte-ordering");

> `

This will create a new exception object called `EndianError` subclassed on `DataError`, and code that catches the `DataError` exception will additionally catch the `EndianError` exception.

Next Previous Contents