{"record":{"id":"cffbbee95745daca","repo":"hashicorp/consul","slug":"source-must-not-be-nil","errorCode":null,"errorMessage":"source must not be nil","messagePattern":"source must not be nil","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/controller/controller.go","lineNumber":143,"sourceCode":"\n\treturn ctl\n}\n\n// WithQuery will add a named query to the controllers cache for usage during reconcile or in dependency mappers\nfunc (ctl *Controller) WithQuery(queryName string, fn cache.Query) *Controller {\n\t_, duplicate := ctl.queries[queryName]\n\tif duplicate {\n\t\tpanic(fmt.Sprintf(\"a predefined cache query with name %q already exists\", queryName))\n\t}\n\n\tctl.queries[queryName] = fn\n\treturn ctl\n}\n\n// WithCustomWatch adds a new custom watch. Custom watches do not affect the controller cache.\nfunc (ctl *Controller) WithCustomWatch(source *Source, mapper CustomDependencyMapper) *Controller {\n\tif source == nil {\n\t\tpanic(\"source must not be nil\")\n\t}\n\n\tif mapper == nil {\n\t\tpanic(\"mapper must not be nil\")\n\t}\n\n\tctl.customWatches = append(ctl.customWatches, customWatch{source, mapper})\n\treturn ctl\n}\n\n// WithLogger changes the controller's logger.\nfunc (ctl *Controller) WithLogger(logger hclog.Logger) *Controller {\n\tif logger == nil {\n\t\tpanic(\"logger must not be nil\")\n\t}\n\n\tctl.logger = logger\n\treturn ctl","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/internal/controller/controller.go#L125-L161","documentation":"Controller.WithCustomWatch attaches a watch backed by an external event Source (it feeds the controller directly instead of the shared cache). A nil Source is rejected with a panic at setup time: the controller would have nothing to consume and every attempt to start the watch would nil-panic inside Run().","triggerScenarios":"Calling WithCustomWatch(nil, mapper) — usually a Source variable that was never constructed, a source builder that returned nil on error, or scaffolding code with a placeholder nil before the source exists.","commonSituations":"Building custom sources in a later iteration and leaving nil placeholders; error-swallowing source constructors; conditional setup where one path forgets to build the source.","solutions":["Construct the Source first and pass it: WithCustomWatch(buildSource(cfg), mapper)","If a constructor can fail, handle the error and abort setup instead of passing nil","Order setup so WithCustomWatch is only called once the source is fully built"],"exampleFix":"// before\nvar src *controller.Source\nctl.WithCustomWatch(src, mapper) // panic: source must not be nil\n\n// after\nsrc := controller.NewSource(...) // construct first\nctl.WithCustomWatch(src, mapper)","handlingStrategy":"validation","validationCode":"// build and validate the source before wiring\nsrc, err := buildSource(cfg)\nif err != nil {\n    return fmt.Errorf(\"custom watch source: %w\", err)\n}\nif src == nil {\n    return fmt.Errorf(\"custom watch source must not be nil\")\n}\nctl.WithCustomWatch(src, mapper)","typeGuard":"func isNilSource(s *controller.Source) bool {\n    return s == nil\n}","tryCatchPattern":null,"preventionTips":["Make source constructors return errors instead of nil","Call WithCustomWatch only after the source is fully built","Add placeholder sources behind a not-implemented error, never a nil pointer"],"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"}