20.2.1 Signaling conditions

Dylan provides a structured mechanism for indicating that an unusual event or exceptional situation has occurred during the execution of a program. Using this mechanism is called signaling a condition. A condition is an instance of the <condition> class, which represents a problem or unusual situation encountered during program execution.

To signal a condition, we need to take these steps:

1. Define a condition class, which must be a subclass of <condition>. The condition class should have slots that are appropriate for the application. In this example, we define a condition class named <time-error> to be a direct subclass of <error>. Note that <error> is a subclass of <condition>. We defined <time-error> to inherit from <error>, because in case our application does not handle the exception, we want Dylan always to take some action, such as entering a debugger. If <time-error> inherited from <condition> and the application failed to handle the exception, then the exception might simply be ignored.

2. Modify the functions that might detect the exception. These functions must make an instance of the condition class, and must use an appropriate Dylan function to initiate the signaling process. In this example, we redefine the + method to signal the condition with the error function.

In the following code, we define a condition named <time-error> to represent any kind of time error, and we define a condition named <time-boundary-error> to represent violations of time-of-day bounds.

define abstract class <time-error> (<error>)
  constant slot invalid-time :: <time>, required-init-keyword: invalid-time:;
end class <time-error>;
define method say (condition :: <time-error>) => ()
  format-out("The time ");
  say(condition.invalid-time);
  format-out(" is invalid.");
end method say;
define class <time-boundary-error> (<time-error>)
  // Inclusive bound
  constant slot min-valid-time 
    :: <time>, required-init-keyword: min-time:;
  // Exclusive bound
  constant slot valid-time-limit 
    :: <time>, required-init-keyword: time-limit:;
end class <time-boundary-error>;
define method say (condition :: <time-boundary-error>) => ()
  next-method();
  format-out("\nIt must not be less than ");
  say(condition.min-valid-time);
  format-out(" and must be less than ");
  say(condition.valid-time-limit);
  format-out(".");
end method say;

We redefine the + method to signal the <time-boundary-error> condition (instead of returning an error string) to indicate that this problem has occurred:

define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>)
  => (sum :: <time-of-day>)
  let sum 
    = make(<time-of-day>, 
           total-seconds: 
             offset.total-seconds + time-of-day.total-seconds);
  if (sum >= $midnight & sum < $tomorrow)
    sum;
  else 
    error(make(<time-boundary-error>, invalid-time: sum,
               min-time: $midnight, time-limit: $tomorrow));
  end if;
end method \+;

We create the condition with make, just as we create instances of other classes. We call the error function to signal the condition. The error function is guaranteed never to return to its caller.

Now we can specify an exact return value for the + method, because we are no longer returning an error string to indicate a problem with the addition.

In previous chapters (for example, in Section 6.1.3, page 78), we called the error function with a string. Given a string as its first argument, the error function creates a general-purpose condition named <simple-error> and stores its arguments in the condition instance. In the preceding example, however, we created an instance of a condition that is customized for our program (<time-boundary-error>), and then supplied that condition to the error function. This approach provides information that is more readily accessible to the code that will handle the condition. Conditions, like any other Dylan class, can use inheritance, and can participate in generic function dispatch. For example, we define say methods for our errors, so that our handlers can provide a reasonable error message to the user. (Unfortunately, Dylan debuggers do not yet have a standard way to know about our say generic function. We expect that Dylan will eventually support such a mechanism.)

Supplying a specific condition to the error function brings the full power of Dylan's object-oriented programming capabilities to the task of signaling and handling exceptional situations.

Once the error function receives a condition instance, or makes an instance of <simple-error> itself, Dylan begins a process of attempting to resolve the situation represented by the condition. We present the details of condition resolution in the next section.