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.

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:

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:

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.