hashicorp/consul · error

reconciler must not be nil

Error message

reconciler must not be nil

What it means

Controller.WithReconciler sets the reconciler that processes mapped resources in Consul's controller framework. A nil reconciler is rejected with a panic because a controller without one can never process work — the builder API deliberately panics on misuse so setup errors surface immediately instead of as nil-pointer panics inside Run().

Source

Thrown at internal/controller/controller.go:100

// WithNotifyStart registers a callback to be run when the controller is being started.
// This happens prior to watches being started and with a fresh cache.
func (ctl *Controller) WithNotifyStart(start RuntimeCallback) *Controller {
	ctl.startCb = start
	return ctl
}

// WithNotifyStop registers a callback to be run when the controller has been stopped.
// This happens after all the watches and mapper/reconcile queues have been stopped. The
// cache will contain everything that was present when we started stopping watches.
func (ctl *Controller) WithNotifyStop(stop RuntimeCallback) *Controller {
	ctl.stopCb = stop
	return ctl
}

// WithReconciler changes the controller's reconciler.
func (ctl *Controller) WithReconciler(reconciler Reconciler) *Controller {
	if reconciler == nil {
		panic("reconciler must not be nil")
	}

	ctl.reconciler = reconciler
	return ctl
}

// WithWatch enables watching of the specified resource type and mapping it to the managed type
// via the provided DependencyMapper. Extra cache indexes to calculate on the watched type
// may also be provided.
func (ctl *Controller) WithWatch(watchedType *pbresource.Type, mapper DependencyMapper, indexes ...*index.Index) *Controller {
	key := resource.ToGVK(watchedType)

	_, alreadyWatched := ctl.watches[key]
	if alreadyWatched {
		panic(fmt.Sprintf("resource type %q already has a configured watch", key))
	}

	w := newWatch(watchedType, mapper)

View on GitHub (pinned to 2397ff0d76)

Solutions

  1. Always construct and pass a real reconciler before Run()
  2. Restructure conditionals so the reconciler is created unconditionally, or bail out of setup entirely when it cannot be
  3. Guard before wiring: if r == nil { return errors.New("reconciler required") }

Example fix

// before
var r controller.Reconciler
if cfg.Enabled { r = &Reconciler{} }
ctl.WithReconciler(r) // panic when cfg.Enabled is false

// after
ctl.WithReconciler(&Reconciler{}) // construct unconditionally
// or gate the whole controller: if !cfg.Enabled { return nil }
Defensive patterns

Strategy: validation

Validate before calling

// validate before wiring
if reconciler == nil {
    return fmt.Errorf("controller %q requires a reconciler", name)
}
ctl := controller.NewController(managedType, opts...).WithReconciler(reconciler)

Type guard

// also catches typed-nil reconcilers hidden in the interface
func isNilReconciler(r Reconciler) bool {
    if r == nil {
        return true
    }
    v := reflect.ValueOf(r)
    switch v.Kind() {
    case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func:
        return v.IsNil()
    }
    return false
}

Prevention

When it happens

Trigger: Calling ctl.WithReconciler(nil), most often with an interface variable that was conditionally assigned and stayed nil: var r Reconciler; if cfg.Enabled { r = &myReconciler{} } ... WithReconciler(r).

Common situations: Feature-gated controller construction; refactored setup code where one branch forgets to assign; placeholder nil left from scaffolding example controllers.

Related errors


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/f4066512f0ef2919. Report an issue: GitHub.