{"record":{"id":"2b1a11e442a8ccb7","repo":"hashicorp/consul","slug":"mapper-must-not-be-nil","errorCode":null,"errorMessage":"mapper must not be nil","messagePattern":"mapper must not be nil","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/controller/controller.go","lineNumber":147,"sourceCode":"// 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\n}\n\n// WithBackoff changes the base and maximum backoff values for the controller's\n// retry rate limiter.","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/hashicorp/consul/blob/2397ff0d763d34f2fe37fe59fde6a7f7fc430a3e/internal/controller/controller.go#L129-L165","documentation":"Controller.WithCustomWatch requires both a Source and a CustomDependencyMapper. A nil mapper panics at setup: events from the custom source would arrive with no way to map them onto the managed resource type, so the controller could never enqueue work — the constructor catches this misuse up front.","triggerScenarios":"Calling WithCustomWatch(src, nil) — a mapper variable declared but never assigned, a conditional branch that skips mapper construction, or scaffolding code where the mapper is still to be written.","commonSituations":"Incrementally building custom watches and leaving the mapper for later; refactoring mappers into helpers that can return nil; feature-gated mapper construction.","solutions":["Implement and pass a CustomDependencyMapper before starting the controller","If mapping is conditional, still provide a mapper that filters and returns nothing rather than nil","Guard construction: if mapper == nil { return errors.New(\"mapper required\") }"],"exampleFix":"// before\nctl.WithCustomWatch(src, nil) // panic: mapper must not be nil\n\n// after\ntype mapper struct{}\nfunc (mapper) Map(watched, rt *pbresource.Resource) ([]reconcile.Request, error) { ... }\nctl.WithCustomWatch(src, mapper{})","handlingStrategy":"validation","validationCode":"// validate the mapper before wiring\nif mapper == nil {\n    return fmt.Errorf(\"custom watch requires a non-nil DependencyMapper\")\n}\nctl.WithCustomWatch(src, mapper)","typeGuard":"func isNilMapper(m controller.CustomDependencyMapper) bool {\n    if m == nil {\n        return true\n    }\n    v := reflect.ValueOf(m)\n    return v.Kind() == reflect.Ptr && v.IsNil()\n}","tryCatchPattern":null,"preventionTips":["Write the mapper before registering the watch; no placeholder nils","For conditional mapping, provide a mapper that filters and returns empty requests","Smoke-test controller construction so missing mappers fail at build time"],"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"}