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

  1. Pass hclog.NewNullLogger() (tests) or hclog.Default() when no real logger is configured
  2. Initialize the hclog.Logger variable (hclog.New(&hclog.LoggerOptions{...})) before building the controller
  3. Check the error of any function that returns a logger before passing it to WithLogger
  4. 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

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


AI-assisted analysis of hashicorp/consul@2397ff0d76 (2026-08-15). Data as JSON: /api/errors/dedc8b24f9b9c705. Report an issue: GitHub.