vitessio/vitess · error

listener must be a function

Error message

listener must be a function

What it means

event.AddListener reflects on the listener function to validate it takes exactly one input argument matching the dispatched event type; a listener that is not a func at all is rejected immediately with BadListenerError panic. Since this is a programming error, the library panics rather than returning an error.

Source

Thrown at go/event/event.go:111

func (why BadListenerError) Error() string {
	return "bad listener func: " + string(why)
}

// AddListener registers a listener function that will be called when a matching
// event is dispatched. The type of the function's first (and only) argument
// declares the event type (or interface) to listen for.
func AddListener(fn any) {
	listenersMutex.Lock()
	defer listenersMutex.Unlock()

	fnType := reflect.TypeOf(fn)

	// check that the function type is what we think: # of inputs/outputs, etc.
	// panic if conditions not met (because it's a programming error to have that happen)
	switch {
	case fnType.Kind() != reflect.Func:
		panic(BadListenerError("listener must be a function"))
	case fnType.NumIn() != 1:
		panic(BadListenerError("listener must take exactly one input argument"))
	}

	// the first input parameter is the event
	evType := fnType.In(0)

	// keep a list of listeners for each event type
	listeners[evType] = append(listeners[evType], fn)

	// if eventType is an interface, store it in a separate list
	// so we can check non-interface objects against all interfaces
	if evType.Kind() == reflect.Interface {
		interfaces = append(interfaces, evType)
	}
}

// Dispatch sends an event to all registered listeners that were declared

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Pass a function taking exactly one parameter of the event type, e.g. func(ev *MyEvent)
  2. Check the value being registered is a func (see type guard below)
  3. Fix the call site so the handler signature matches the event's documented contract

Example fix

// before
event.AddListener(listeners, myStruct)
// after
event.AddListener(listeners, func(ev *myEvent) { /* handle */ })
Defensive patterns

Strategy: type-guard

Validate before calling

func safeAddListener(ls event.Listeners, fn any) {
    if reflect.TypeOf(fn) != nil && reflect.TypeOf(fn).Kind() == reflect.Func {
        event.AddListener(ls, fn)
    }
}

Type guard

func isFuncListener(fn any) bool {
    t := reflect.TypeOf(fn)
    return t != nil && t.Kind() == reflect.Func
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        if _, ok := r.(event.BadListenerError); ok {
            log.Errorf("invalid listener: %v", r)
        } else {
            panic(r)
        }
    }
}()
event.AddListener(listeners, candidate)

Prevention

When it happens

Trigger: Calling event.AddListener(listeners, x) where x is not a function — e.g. a struct, nil, or a value of non-func type passed by mistake.

Common situations: Passing a method value bound incorrectly; passing the event type instead of a handler; mixing up AddListener with a generic subscribe API that accepts callbacks of any shape.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/d9b0796714fdfa11. Report an issue: GitHub.