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.

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.

Appendix: 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)