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 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.

Example

#!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

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. 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:

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.

Next Steps

The next section on Enumerations shows more of the details of enumerations that have been used in this example.