All Exceptions in EK9 are unchecked, this means they cannot be declared as part of a method or function signature. This keeps signatures clean and stops implementation details leaking out (at least at the point of declaration).
The intention is to support Exceptions in 'exceptional' circumstances. As EK9 has support for returning values that are un set and has support for Optional these two facilities remove the need to use Exceptions to some degree.
Unlike some languages that allow different types of Exceptions to be 'caught'; EK9 only supports a single catch/handle statement followed by an optional finally statement. This is a deliberate design decision as it enables more succinct syntax and dovetails in with the dispatcher mechanism.
All developer created Exception classes must be derived from the standard Exception class. Exceptions are not intended to be used for normal flow control, there are lots of other flow control mechanisms in EK9 for this. The Exception is designed to be used for 'exception flow control', this means when your normal expected flow cannot be followed.
The dispatcher mechanism must be employed to process specific Exceptions. In general it is best to avoid processing specific exceptions in some sort of 'case statement'. Where ever possible use polymorphic operators and methods on the Exception class.
Where this is not possible, employ the dispatcher to extract the details from the specific Exception. The following example illustrates how this can be done.
The example defines a simple enumerated type, two specific developer defined 'Exceptions' that have additional properties and accessor methods. There are also two classes to demonstrate the features of Exceptions and a program as a Driver to trigger the Exceptions.
#!ek9 module introduction defines type BigCat Lion Tiger WildCat Leopard Lynx defines class AnException extends Exception supportingInformation String: String() AnException() -> primaryReason as String supportingInformation as String exitCode as Integer super(primaryReason, exitCode) this.supportingInformation :=: supportingInformation supportingInformation() <- rtn as String: supportingInformation override operator $ <- rtn as String: information() + " " + supportingInformation() + " exit code " + $exitCode() OtherException extends Exception retryAfter as DateTime: DateTime() OtherException() -> reason as String retryAfter as DateTime super(reason) this.retryAfter :=: retryAfter retryAfter() <- rtn as DateTime: retryAfter override operator $ <- rtn as String: information() + " retry after " + $retryAfter() ExceptionExample clock as Clock deferProcessingUntilAfter as DateTime: DateTime() private ExceptionExample() assert false ExceptionExample() -> clock as Clock this.clock: clock checkExceptionHandling() -> cat as BigCat <- didProcess as Boolean: false stdout <- Stdout() stderr <- Stderr() try if deferProcessing() stdout.println("Deferred until after " + $deferProcessingUntilAfter + " " + $cat + " not processed") else stdout.println(triggerPossibleException(cat)) didProcess: true catch -> ex as Exception errorMessage <- handleException(ex) stderr.println(errorMessage) finally stdout.println("Finished checking " + $cat) triggerPossibleException() -> cat as BigCat <- rtn as String: String() switch cat case BigCat.Lion throw Exception($cat, 1) case BigCat.Tiger throw AnException("Too dangerous", $cat, 2) case BigCat.Leopard throw OtherException($cat, clock.dateTime() + PT2H) default rtn: "Success with " + $cat deferProcessing() <- rtn as Boolean: false if deferProcessingUntilAfter? rtn: deferProcessingUntilAfter > clock.dateTime() private handleException() as dispatcher -> ex as Exception <- rtn as String: $ex private handleException() -> ex as AnException <- rtn as String: $ex if ex.exitCode()? tidyUpReadyForProgramExit() private handleException() -> ex as OtherException <- rtn as String: $ex this.deferProcessingUntilAfter: ex.retryAfter() private tidyUpReadyForProgramExit() Stdout().println("Would tidy up any state ready for program exit") FileExceptionExample demonstrateFileNotFound() stdout <- Stdout() stderr <- Stderr() file <- TextFile("MightNotExist.txt") try -> input <- file.input() cat input > stdout //rather than use catch 'handle' can be used handle -> ex as Exception stderr.println($ex) finally stdout.println("Automatically closed file if opened") demonstrateFilesNotFound() file1 <- TextFile("MightNotExist.txt") file2 <- TextFile("AlsoMightNotExist.txt") mainResults <- try -> input1 <- file1.input() input2 <- file2.input() <- rtn as String := String() results <- cat input1 | collect as List of String cat input2 >> results rtn: $results //Let the exceptions fly back - don't handle in here. Stdout().println("main Results [" + mainResults + "]") defines program TryCatchExample() stdout <- Stdout() stderr <- Stderr() //Rather than use SystemClock - simulate one so that date time can be altered. simulatedClock <- () with trait of Clock currentDateTime as DateTime: 1971-02-01T12:00:00Z override dateTime() <- rtn as DateTime: currentDateTime setCurrentDateTime() -> newDateTime as DateTime this.currentDateTime = newDateTime //use the simulated clock example1 <- ExceptionExample(simulatedClock) for cat in BigCat if example1.checkExceptionHandling(cat) stdout.println("Processing of " + $cat + " was completed") else stderr.println("Processing of " + $cat + " was NOT completed") //just try Lynx again assert ~ example1.checkExceptionHandling(BigCat.Lynx) //alter the time just passed the retry after time. simulatedClock.setCurrentDateTime(simulatedClock.dateTime() + PT2H1M) //Now it should be processed. assert example1.checkExceptionHandling(BigCat.Lynx) example2 <- FileExceptionExample() example2.demonstrateFileNotFound() try example2.demonstrateFilesNotFound() catch -> ex as Exception Stderr().println("TryCatchExample: " + $ex) //EOF
The results from the example above are show below.
With standard output as follows:
Finished checking Lion Would tidy up any state ready for program exit Finished checking Tiger Success with WildCat Finished checking WildCat Processing of WildCat was completed Finished checking Leopard Processing of Leopard was NOT completed Deferred until after 1971-02-01T14:00:00Z Lynx not processed Finished checking Lynx Deferred until after 1971-02-01T14:00:00Z Lynx not processed Finished checking Lynx Success with Lynx Finished checking Lynx Automatically closed file if opened
With error output as follows:
Exception: Lion Processing of Lion was NOT completed Too dangerous Tiger exit code 2 Processing of Tiger was NOT completed Leopard retry after 1971-02-01T14:00:00Z Processing of Lynx was NOT completed Exception: File Not Found: MightNotExist.txt TryCatchExample: Exception: File Not Found: MightNotExist.txt
While this example is a little contrived, there are a couple of points of interest.
- A simulated clock (dynamic class) has been used for testing
- handle can be used in place of catch - they have the same meaning
- It is possible to just use try without catch/handle or finally
- Both try and catch/handle can be used without finally
- Try and finally can be used without catch/handle
- There can only be one catch/handle clause
- Try can be used like an expression to return a value
- Try can be used with parameters that 'open' resources and will automatically call 'close' on those resources.
By incorporating the dispatcher mechanism into the EK9 language it has been possible to remove any need for 'casting' and 'instanceof' checking. As shown in the example above, where specific classes have additional methods/information; that information can be accessed. Indeed it is possible to extract that information and hold it as state in the class if necessary.
The other main point is to ensure that it is not always necessary to access specific class methods if that can be avoided (note the overridden $ operator in the Exceptions classes).
Like most other languages that support Exceptions, EK9:
- Keeps throwing the Exception up the call stack until it is caught. The main program will exit if it is not caught
- The Exception class supports holding an 'exit code', if the Exception goes back to the main program the application will exit with that code.
While the try, catch, finally and Exception control looks much like those in other languages, EK9 does add quite a few features, but also removes the 'multi-catch' nature and provides the dispatcher instead.
This latter restriction forces the specific 'Exception' processing to either be very standard and simple, or to be delegated to class methods via the dispatcher. While this may appear inconvenient, it forces 'separation' of receiving the 'Exception' and dealing with a range of logic of what to do with the fact the 'Exception' has occurred.
The next section on Enumerations shows more of the details of enumerations that have been used in this example.