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
- Always construct and pass a real reconciler before Run()
- Restructure conditionals so the reconciler is created unconditionally, or bail out of setup entirely when it cannot be
- 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
- Construct the reconciler unconditionally before controller wiring
- Avoid conditional 'var r Reconciler' patterns; early-return instead
- Cover controller setup with a smoke test that fails on missing wiring
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
- no indexer was supplied when creating a new cache Index
- source must not be nil
- mapper must not be nil
- all indexers must have a non-empty name
- The Indexer must also implement one of the SingleIndexer or
AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15).
Data as JSON: /api/errors/f4066512f0ef2919.
Report an issue: GitHub.