{"record":{"id":"f4066512f0ef2919","repo":"hashicorp/consul","slug":"reconciler-must-not-be-nil","errorCode":null,"errorMessage":"reconciler must not be nil","messagePattern":"reconciler must not be nil","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/controller/controller.go","lineNumber":100,"sourceCode":"// WithNotifyStart registers a callback to be run when the controller is being started.\n// This happens prior to watches being started and with a fresh cache.\nfunc (ctl *Controller) WithNotifyStart(start RuntimeCallback) *Controller {\n\tctl.startCb = start\n\treturn ctl\n}\n\n// WithNotifyStop registers a callback to be run when the controller has been stopped.\n// This happens after all the watches and mapper/reconcile queues have been stopped. The\n// cache will contain everything that was present when we started stopping watches.\nfunc (ctl *Controller) WithNotifyStop(stop RuntimeCallback) *Controller {\n\tctl.stopCb = stop\n\treturn ctl\n}\n\n// WithReconciler changes the controller's reconciler.\nfunc (ctl *Controller) WithReconciler(reconciler Reconciler) *Controller {\n\tif reconciler == nil {\n\t\tpanic(\"reconciler must not be nil\")\n\t}\n\n\tctl.reconciler = reconciler\n\treturn ctl\n}\n\n// WithWatch enables watching of the specified resource type and mapping it to the managed type\n// via the provided DependencyMapper. Extra cache indexes to calculate on the watched type\n// may also be provided.\nfunc (ctl *Controller) WithWatch(watchedType *pbresource.Type, mapper DependencyMapper, indexes ...*index.Index) *Controller {\n\tkey := resource.ToGVK(watchedType)\n\n\t_, alreadyWatched := ctl.watches[key]\n\tif alreadyWatched {\n\t\tpanic(fmt.Sprintf(\"resource type %q already has a configured watch\", key))\n\t}\n\n\tw := newWatch(watchedType, mapper)","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/internal/controller/controller.go#L82-L118","documentation":"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().","triggerScenarios":"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).","commonSituations":"Feature-gated controller construction; refactored setup code where one branch forgets to assign; placeholder nil left from scaffolding example controllers.","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\") }"],"exampleFix":"// before\nvar r controller.Reconciler\nif cfg.Enabled { r = &Reconciler{} }\nctl.WithReconciler(r) // panic when cfg.Enabled is false\n\n// after\nctl.WithReconciler(&Reconciler{}) // construct unconditionally\n// or gate the whole controller: if !cfg.Enabled { return nil }","handlingStrategy":"validation","validationCode":"// validate before wiring\nif reconciler == nil {\n    return fmt.Errorf(\"controller %q requires a reconciler\", name)\n}\nctl := controller.NewController(managedType, opts...).WithReconciler(reconciler)","typeGuard":"// also catches typed-nil reconcilers hidden in the interface\nfunc isNilReconciler(r Reconciler) bool {\n    if r == nil {\n        return true\n    }\n    v := reflect.ValueOf(r)\n    switch v.Kind() {\n    case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func:\n        return v.IsNil()\n    }\n    return false\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["go","consul","controller","panic","programmer-error","nil-safety"],"backgroundTag":null,"analyzedSha":"2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e","analyzedAt":"2026-08-15T19:19:47.700Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}