hashicorp/consul · error
logger must not be nil
Error message
logger must not be nil
What it means
The Consul controller builder method WithLogger (internal/controller/controller.go:155) panics when its logger argument is nil. NewController does not set a default logger, so the builder chain expects you to supply a valid hclog.Logger. The panic is a fail-fast guard: a nil logger stored on the Controller would otherwise cause nil-pointer dereferences later in the reconcile loop, far from the construction site.
Source
Thrown at internal/controller/controller.go:157
// WithCustomWatch adds a new custom watch. Custom watches do not affect the controller cache.
func (ctl *Controller) WithCustomWatch(source *Source, mapper CustomDependencyMapper) *Controller {
if source == nil {
panic("source must not be nil")
}
if mapper == nil {
panic("mapper must not be nil")
}
ctl.customWatches = append(ctl.customWatches, customWatch{source, mapper})
return ctl
}
// WithLogger changes the controller's logger.
func (ctl *Controller) WithLogger(logger hclog.Logger) *Controller {
if logger == nil {
panic("logger must not be nil")
}
ctl.logger = logger
return ctl
}
// WithBackoff changes the base and maximum backoff values for the controller's
// retry rate limiter.
func (ctl *Controller) WithBackoff(base, max time.Duration) *Controller {
ctl.baseBackoff = base
ctl.maxBackoff = max
return ctl
}
// WithPlacement changes where and how many replicas of the controller will run.
// In the majority of cases, the default placement (one leader elected instance
// per cluster) is the most appropriate and you shouldn't need to override it.
func (ctl *Controller) WithPlacement(placement Placement) *Controller {View on GitHub (pinned to 2397ff0d76)
Solutions
- Pass hclog.NewNullLogger() (tests) or hclog.Default() when no real logger is configured
- Initialize the hclog.Logger variable (hclog.New(&hclog.LoggerOptions{...})) before building the controller
- Check the error of any function that returns a logger before passing it to WithLogger
- Derive a sub-logger from an existing one, e.g. parentlogger.Named("controller-name")
Example fix
// before
var logger hclog.Logger // nil
ctl := controller.NewController("demo", typ).WithLogger(logger) // panics
// after
if logger == nil {
logger = hclog.NewNullLogger()
}
ctl := controller.NewController("demo", typ).WithLogger(logger) Defensive patterns
Strategy: validation
Validate before calling
if logger == nil {
logger = hclog.NewNullLogger() // or hclog.Default()
}
ctl := controller.NewController(name, managedType).WithLogger(logger) Try / catch
// Go has no try/catch; wrap construction so the panic becomes an error
func buildController(logger hclog.Logger) (ctl *controller.Controller, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("controller construction failed: %v", r)
}
}()
ctl = controller.NewController("demo", typ).WithLogger(logger)
return
} Prevention
- Never let a (hclog.Logger, error) return pair flow into WithLogger unchecked
- Standardize on hclog.NewNullLogger() as the test default so nil never appears
- Treat all With* builder panics in this package as constructor contracts: validate inputs at the call site
When it happens
Trigger: Calling controller.NewController(name, typ).WithLogger(nil); passing an hclog.Logger variable that was declared but never initialized; calling a helper that returns (hclog.Logger, error) and passing its nil result without checking the error.
Common situations: Test wiring where the logger was omitted; refactors that move logger creation after controller construction; helper functions that return nil on error and callers that forward the value unchecked.
Related errors
- all indexers must have a non-empty name
- no indexer was supplied when creating a new cache Index
- The Indexer must also implement one of the SingleIndexer or
- reconciler must not be nil
- resource type %q already has a configured watch
AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15).
Data as JSON: /api/errors/dedc8b24f9b9c705.
Report an issue: GitHub.