E05230: dispatcher has ambiguous handler resolution for reachable type
What went wrong
A dispatcher has two or more handlers that match a reachable type with equal cost, making it impossible to determine which handler should be called. The dispatcher entry point (marked 'as dispatcher') acts as the fallback of last resort -- it only handles types that no specific handler matches. Specific handlers are preferred over the entry point.
When two handlers match at equal cost, the compiler cannot determine which should win. This is a declaration-time error: the ambiguity exists in the type hierarchy regardless of what values are actually dispatched.
Dispatchers work best with tree hierarchies (class inheritance) where every type has exactly one path to the base. Traits create DAG hierarchies (multiple inheritance paths), which is where ambiguity becomes structural. Choose the resolution strategy that best fits the scale and ownership of the type hierarchy.
Worked examples
Compilable examples demonstrating this error and the correct fix:
- ek9 -q 870 (How does adding a concrete type handler resolve dispatche...)
[RESOLUTION STRATEGY 1: Explicit Handler] Add a specific handler for each ambiguous concrete type: a handler for Duck itself is an exact match (cost 0.0) and beats both trait handlers (0.10). Best when: few ambiguous types, dispatcher owns all types (good.ek9 in the [EXAMPLE] section below).
Limitation: does not scale. Adding a new multi-trait type (Pelican, Platypus) breaks the dispatcher again. Each new type needs a handler.
[RESOLUTION STRATEGY 2: Bridge Trait] Create a combining trait that encompasses both competing traits. Ambiguous types implement the bridge trait instead of individual traits. Best when: you control the type hierarchy and can refactor it (good_bridge.ek9 below: Duck has only 'AmphibiousFlyer', which has Flyable and Swimmable, and the dispatcher has a handler for AmphibiousFlyer).
Why it works: Duck -> AmphibiousFlyer is one trait hop (cost 0.10). Duck -> Flyable goes Duck -> AmphibiousFlyer -> Flyable (cost 0.20). The bridge handler wins because it is closer in the trait hierarchy.
CRITICAL: the concrete type must implement ONLY the bridge trait, not the individual traits directly. If Duck implements all three (Flyable, Swimmable, AmphibiousFlyer), all are at cost 0.10 and the ambiguity remains.
[RESOLUTION STRATEGY 3: Decomposed Dispatchers] Split into multiple single-axis dispatchers, each handling one trait dimension. The caller chooses which dimension to dispatch on. Best when: you cannot modify the type hierarchy, or traits represent genuinely independent concerns. A SerializationDispatcher whose 'serialize()' dispatches over Entity (with a handler for UserProfile), beside a RenderDispatcher whose 'render()' dispatches over Entity (with a handler for Dashboard), each handles one behavioural axis. A type implementing both Serializable and Renderable is dispatched by the appropriate dispatcher for the operation being performed. No ambiguity because each dispatcher has a single concern.
Choosing a strategy
- Few ambiguous types, simple hierarchy -> Strategy 1 (explicit handler)
- You own the trait hierarchy and can refactor -> Strategy 2 (bridge trait)
- Traits represent independent concerns -> Strategy 3 (decomposed dispatchers)
- Large hierarchy with many multi-trait types -> Strategy 3 (always scales)
Common causes
- A class implements two traits and both traits have handlers
- Two classes at the same hierarchy depth both have handlers
- Adding a trait handler without considering which types implement it
- Growing a type hierarchy without re-evaluating dispatcher coverage
See also
Examples
Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.
Fails with E05230: dispatcher has ambiguous handler resolution for reachable type
#!ek9 defines module error.e05230.bad defines trait Flyable fly() Stdout().println("Flying") Swimmable swim() Stdout().println("Swimming") defines class Animal as abstract speak() as abstract Dog is Animal override speak() Stdout().println("Woof") Cat is Animal override speak() Stdout().println("Meow") <?- Duck implements both Flyable and Swimmable. This means handlers for Flyable and Swimmable both match Duck at equal cost. -?> Duck is Animal with trait of Flyable, Swimmable override speak() Stdout().println("Quack") <?- AnimalProcessor has handlers for Flyable and Swimmable but no handler for Duck. Since Duck implements both traits at equal cost (0.10), the dispatcher has ambiguous resolution for Duck. -?> AnimalProcessor process() as dispatcher -> a as Animal a.speak() process() -> a as Dog a.speak() process() -> a as Flyable a.fly() process() -> a as Swimmable a.swim()
Fixed: Add a handler for the type that matches two handlers equally - here Duck
#!ek9 defines module error.e05230.good defines trait Flyable fly() Stdout().println("Flying") Swimmable swim() Stdout().println("Swimming") defines class Animal as abstract speak() as abstract Dog is Animal override speak() Stdout().println("Woof") Duck is Animal with trait of Flyable, Swimmable override speak() Stdout().println("Quack") AnimalProcessor process() as dispatcher -> animal as Animal animal.speak() process() -> animal as Dog animal.speak() process() -> animal as Duck animal.fly() animal.swim() process() -> animal as Flyable animal.fly() process() -> animal as Swimmable animal.swim() //Duck is both Flyable and Swimmable: this handler says which wins
Fixed: A bridge trait: the multi-trait type takes ONLY the trait that combines the others
#!ek9 defines module error.e05230.good.bridge defines trait Flyable fly() Stdout().println("Flying") Swimmable swim() Stdout().println("Swimming") //The bridge: inherits fly() and swim() AmphibiousFlyer with trait of Flyable, Swimmable defines class Animal as abstract speak() as abstract //Duck takes ONLY the bridge trait, never Flyable or Swimmable directly Duck is Animal with trait of AmphibiousFlyer override speak() Stdout().println("Quack") AnimalProcessor process() as dispatcher -> animal as Animal animal.speak() process() -> animal as Flyable animal.fly() process() -> animal as Swimmable animal.swim() //One trait hop from Duck: closer than Flyable or Swimmable (two hops) process() -> animal as AmphibiousFlyer animal.fly() animal.swim()
Questions and answers
These Q&A entries show E05230 and its fix: