How do sealed dispatchers enforce exhaustive handler coverage?

← Dispatcher Validation · Ref: Q618

When a dispatcher's parameter type is a sealed type (one with 'allow only'), the compiler requires handlers for ALL permitted concrete types. Missing a handler raises E05260.

EXHAUSTIVE CHECKING

The compiler cross-references the 'allow only' list with the set of dispatcher handlers. Every concrete type in the list must have a matching handler (either exact or through a supertype).

COMPILE-TIME SAFETY

When you add a new type to the 'allow only' list, EVERY dispatcher operating on that type fails to compile until a handler is added. This guarantees all dispatch points are updated when the type hierarchy grows.

UNSEALED DISPATCHERS

Dispatchers on unsealed types (without 'allow only') do NOT require exhaustive handlers. The entry method acts as a catch-all fallback for unhandled types.

ABSTRACT TYPES

Abstract types in the hierarchy do not need handlers because they cannot be instantiated at runtime. Only concrete types require handlers.

BOTH CLASSES AND TRAITS

Exhaustive checking works for both sealed classes (allow only on class) and sealed traits (allow only on trait). The mechanism is identical.

See Q299 for sealed trait dispatchers. See Q302 for sealed class dispatchers. See Q614 for handler type hierarchy. See Q607 for sealed class requirements.

Example

defines module qa.dispatchervalidation.exhaustive

  defines trait

    //Sealed trait: requires exhaustive dispatch
    Command allow only CreateCommand, UpdateCommand, DeleteCommand
      execute() as abstract
        <- rtn as String?

  defines class

    CreateCommand with trait of Command
      override execute()
        <- rtn as String: "created"

    UpdateCommand with trait of Command
      override execute()
        <- rtn as String: "updated"

    DeleteCommand with trait of Command
      override execute()
        <- rtn as String: "deleted"

    //CORRECT: exhaustive dispatcher handles ALL permitted types
    CommandProcessor
      process() as dispatcher
        -> cmd as Command
        <- rtn as String: cmd.execute()

      process()
        -> cmd as CreateCommand
        <- rtn as String: "Processing create: " + cmd.execute()

      process()
        -> cmd as UpdateCommand
        <- rtn as String: "Processing update: " + cmd.execute()

      process()
        -> cmd as DeleteCommand
        <- rtn as String: "Processing delete: " + cmd.execute()

  defines function

    testExhaustiveDispatch()
      processor <- CommandProcessor()

      createCmd <- CreateCommand()
      updateCmd <- UpdateCommand()
      deleteCmd <- DeleteCommand()

      require processor.process(createCmd) == "Processing create: created"
      require processor.process(updateCmd) == "Processing update: updated"
      require processor.process(deleteCmd) == "Processing delete: deleted"

Common mistakes

E50060 — String has no toUpperCase() method in EK9. Use upperCase() instead. See ek9 -h E50060 for details.

Incorrect:

require processor.process(createCmd).toUpperCase() == "Processing create: created"

Correct:

require processor.process(createCmd) == "Processing create: created"

E50001 — The dispatcher entry takes Command. A handler for String is outside the Command hierarchy. All handler parameter types must be subtypes of the entry parameter type. See ek9 -h E50001 for details.

Incorrect:

process()
        -> item as String

Correct:

process()
        -> cmd as CreateCommand

E05260 — The sealed trait Command allows only CreateCommand, UpdateCommand, and DeleteCommand. Removing the DeleteCommand handler leaves the dispatcher non-exhaustive. All permitted types must have handlers. See ek9 -h E05260 for details.

Incorrect:

      process()
        -> cmd as CreateCommand
        <- rtn as String: "Processing create: " + cmd.execute()

      process()
        -> cmd as UpdateCommand
        <- rtn as String: "Processing update: " + cmd.execute()

Correct:

      process()
        -> cmd as CreateCommand
        <- rtn as String: "Processing create: " + cmd.execute()

      process()
        -> cmd as UpdateCommand
        <- rtn as String: "Processing update: " + cmd.execute()

      process()
        -> cmd as DeleteCommand
        <- rtn as String: "Processing delete: " + cmd.execute()
Other ways to ask this
  • What is E05260 for dispatchers?
  • Must I handle all types in a sealed dispatcher?
  • How does EK9 enforce exhaustive dispatch on sealed types?

Coming from another language?

Java: exhaustive switch on sealed types (Java 21+). Kotlin: exhaustive when on sealed classes/interfaces. Rust: exhaustive match on enums. Scala: exhaustive match on sealed traits. TypeScript: exhaustive switch with never type. EK9: E05260 enforces exhaustive dispatch handler coverage on sealed types.

Keywords: exhaustive, E05260, validate, dispatch, visitor, closed, only, complete, allow, handler, sealed, ambiguity, coverage