How do I handle different exception types in EK9 without casting?

← Error Handling and Exceptions · Ref: Q308

EK9 uses the dispatcher pattern to route different exception types to specific handlers without casting or instanceof checks. Since EK9 has single catch per try block (always catching as base Exception), the dispatcher provides type-safe multi-type exception handling.

THE PATTERN

1. Catch as base Exception in the try/catch block.
2. Pass the caught exception to a dispatcher method.
3. The dispatcher routes to the correct private overload based on actual runtime type.
4. Each overload accesses the specific exception type's methods directly.

DISPATCHER METHOD

Declare the dispatcher with the base Exception type:

  private handleException() as dispatcher
    -> ex as Exception
    <- rtn as String: $ex

This is the fallback for any exception type not specifically handled.

SPECIFIC OVERLOADS

Add private overloads for each exception type to handle:

  private handleException()
    -> ex as NetworkError
    <- rtn as String: "Network: " + ex.host()
  private handleException()
    -> ex as TimeoutError
    <- rtn as String: "Timeout: " + $ex.seconds() + "s"

Each overload receives the correctly typed exception, no casting needed.

USAGE IN CATCH

  try
    riskyOperation()
  catch
    -> ex as Exception
    message <- handleException(ex)
    stderr.println(message)

The dispatcher automatically routes to the correct overload based on the actual exception type.

WHY DISPATCHER

- No casting: each overload receives the correct type.
- No instanceof checks: EK9 does not have instanceof.
- Type-safe: compiler verifies each overload's parameter type.
- Extensible: add new exception types by adding new overloads.
- Clean separation: each handler is a focused, single-purpose method.

See Q138 for exception subtypes. See Q134 for try/catch. See Q136 for throwing exceptions. See Q121 for the dispatcher pattern generally.

Example

defines module qa.errorhandling.exdispatcher

  defines class

    <?-
      Network-related exception with host information.
    -?>
    NetworkError extends Exception
      host <- String()

      NetworkError()
        ->
          reason as String
          host as String
        super(reason)
        this.host :=: host

      host() as pure
        <- rtn as String: host

      default operator ?

    <?-
      Timeout exception with duration information.
    -?>
    TimeoutError extends Exception
      seconds <- Integer()

      TimeoutError()
        ->
          reason as String
          seconds as Integer
        super(reason)
        this.seconds :=: seconds

      seconds() as pure
        <- rtn as Integer: seconds

      default operator ?

    <?-
      Demonstrates the dispatcher pattern for exception handling.
      Catches base Exception then dispatches to type-specific handlers.
    -?>
    ServiceClient
      stderr as Stderr: Stderr()

      <?-
        Simulate an operation that can throw different exception types.
      -?>
      callService()
        -> endpoint as String
        <- rtn as String: String()

        if endpoint == "network"
          ex <- NetworkError("Connection refused", "api.example.com")
          throw ex
        else if endpoint == "timeout"
          ex <- TimeoutError("Request timed out", 30)
          throw ex
        else if endpoint == "generic"
          throw Exception("Unknown service error")

        rtn: "Success from " + endpoint

      <?-
        Try calling the service and handle any exception via dispatcher.
      -?>
      safeCall()
        -> endpoint as String
        <- rtn as String: String()

        try
          rtn: callService(endpoint)
        catch
          -> ex as Exception
          rtn: handleException(ex)

      <?-
        Dispatcher: fallback for unrecognised exception types.
      -?>
      private handleException() as dispatcher
        -> ex as Exception
        <- rtn as String: "General error: " + ex.reason()

      <?-
        Handler for NetworkError: access host-specific information.
      -?>
      private handleException()
        -> ex as NetworkError
        <- rtn as String: `Network error on ${ex.host()}: ${ex.reason()}`

      <?-
        Handler for TimeoutError: access timeout-specific information.
      -?>
      private handleException()
        -> ex as TimeoutError
        <- rtn as String: `Timeout after ${ex.seconds()}s: ${ex.reason()}`

      override operator ? as pure
        <- rtn as Boolean: stderr?

  defines program

    ExceptionDispatcherDemo()
      stdout <- Stdout()

      client <- ServiceClient()

      stdout.println("=== Dispatcher routes exceptions by type ===")

      result1 <- client.safeCall("ok")
      stdout.println("OK endpoint: " + result1)

      result2 <- client.safeCall("network")
      stdout.println("Network endpoint: " + result2)

      result3 <- client.safeCall("timeout")
      stdout.println("Timeout endpoint: " + result3)

      result4 <- client.safeCall("generic")
      stdout.println("Generic endpoint: " + result4)

Common mistakes

E50060 — The method is named host(), not getHost(). EK9 does not follow Java getter naming conventions. See ek9 -h E50060 for details.

Incorrect:

ex.getHost()

Correct:

ex.host()
Other ways to ask this
  • How do I use the dispatcher pattern with exceptions?
  • How do I route exceptions to different handlers in EK9?
  • How does EK9 handle multiple exception types without instanceof?

Coming from another language?

Java: catch blocks with instanceof or multi-catch (catch IOException | SQLException), or visitor pattern. Python: multiple except clauses ordered by specificity. Go: errors.Is/errors.As with type switches. Rust: match on enum variants in Result. Kotlin: multiple catch blocks or when expression with is checks. EK9: dispatcher pattern eliminates casting and instanceof entirely. Single catch block delegates to dispatcher which routes by runtime type.

Keywords: type-safe, casting, instanceof, exception, visitor, routing, catch, overload, handler, polymorphic, sealed, dispatcher, handle