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.