Exception Handling in EK9
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 unset and has support for Optional/Result; these facilities reduce 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. The Exception is designed to be used for 'exception flow control', this means when your normal expected flow cannot be followed due to some extreme circumstance (out of memory, file system full for example).
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.
The example is quite long, but is designed to give you a better feel of how exceptions can be employed.
Example
#!ek9 module introduction defines type BigCat Lion Tiger WildCat Leopard Lynx defines class AnException extends Exception supportingInformation <- String() AnException() -> primaryReason as String supportingInformation as String exitCode as Integer super(primaryReason, exitCode) this.supportingInformation :=: supportingInformation supportingInformation() <- rtn as String: supportingInformation override operator $ as pure <- rtn as String: `${information()} ${supportingInformation()} exit code ${exitCode()}` OtherException extends Exception retryAfter <- DateTime() OtherException() -> reason as String retryAfter as DateTime super(reason) this.retryAfter :=: retryAfter retryAfter() <- rtn as DateTime: retryAfter override operator $ as pure <- rtn as String: information() + " retry after " + $retryAfter() ExceptionExample clock as Clock? deferProcessingUntilAfter <- DateTime() default private ExceptionExample() ExceptionExample() -> clock as Clock this.clock: clock checkExceptionHandling() -> aCat as BigCat <- didProcess <- false stdout <- Stdout() stderr <- Stderr() try if deferProcessing() stdout.println(`Deferred until after ${deferProcessingUntilAfter} ${aCat} not processed`) else triggerResult <- triggerPossibleException(aCat) stdout.println(triggerResult) didProcess: true catch -> ex as Exception errorMessage <- handleException(ex) stderr.println(errorMessage) finally stdout.println(`Finished checking ${aCat}`) triggerPossibleException() -> aCat as BigCat <- rtn as String: String() switch aCat case BigCat.Lion throw Exception($aCat, 1) case BigCat.Tiger throw AnException("Too dangerous", $aCat, 2) case BigCat.Leopard throw OtherException($aCat, clock.dateTime() + PT2H) default rtn: `Success with ${aCat}` deferProcessing() <- rtn as Boolean: deferProcessingUntilAfter? <- deferProcessingUntilAfter > clock.dateTime() else false 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() try -> input <- TextFile("MightNotExist.txt").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") demonstrateNamedFileNotFound() -> fileName as String mainResults <- try -> input1 <- TextFile(fileName).input() <- rtn as List of String: cat input1 | collect as List of String //Let the exceptions fly back - don't handle in here. Stdout().println(`Main Results ${mainResults}]`) demonstrateFilesNotFound() mainResults <- try -> input1 <- TextFile("MightNotExist.txt").input() input2 <- TextFile("AlsoMightNotExist.txt").input() <- rtn as List of String: cat input1, input2 | collect as List of String //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. //This is a way of doing a 'Fake'/'Stub'. simulatedClock <- () with trait of Clock as class 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
Results
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
Discussion
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.
Dispatcher
By incorporating the dispatcher mechanism into the EK9 language it has been possible to remove any need for 'casting' and 'instanceof' checking; it also means there is no need to complicate the switch statement with type checks. As shown in the example above, where specific classes have additional methods/information; that information can be accessed. 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.
Summary
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.
While not shown in the above example it is also possible to use:
- Guarded assignments
- Assignments
- Variable declarations
Here are a couple of examples:
#!ek9 defines module introduction defines function testFunction() -> arg0 as String <- rtn as String: String() defines program TryWithGuard() someVar as String? try someVar ?= testFunction("Steve") then example2 <- FileExceptionExample() example2.demonstrateNamedFileNotFound(someVar) catch -> ex as Exception Stderr().println("TryWithGuard: " + $ex) TryWithAssignment() someVar as String? try someVar: testFunction("Steve") then example2 <- FileExceptionExample() example2.demonstrateNamedFileNotFound(someVar) catch -> ex as Exception Stderr().println("TryWithAssignment: " + $ex) TryWithDeclaration() try someVar <- testFunction("Steve") then example2 <- FileExceptionExample() example2.demonstrateNamedFileNotFound(someVar) catch -> ex as Exception Stderr().println(`TryWithDeclaration: ${ex}`) //This is a 'different' 'someVar' - the previous one is now out of scope. someVar <- false assert someVar?
The approach with the assignment/declaration mechanism is the same as that used in if, switch and for flow control elements.
Next Steps
The next section on Enumerations shows more of the details of enumerations that have been used in this example.