hashicorp/consul · error

source must not be nil

Error message

source must not be nil

What it means

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().

Source

Thrown at internal/controller/controller.go:143

	return ctl
}

// WithQuery will add a named query to the controllers cache for usage during reconcile or in dependency mappers
func (ctl *Controller) WithQuery(queryName string, fn cache.Query) *Controller {
	_, duplicate := ctl.queries[queryName]
	if duplicate {
		panic(fmt.Sprintf("a predefined cache query with name %q already exists", queryName))
	}

	ctl.queries[queryName] = fn
	return ctl
}

// 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

View on GitHub (pinned to 2397ff0d76)

Solutions

  1. Construct the Source first and pass it: WithCustomWatch(buildSource(cfg), mapper)
  2. If a constructor can fail, handle the error and abort setup instead of passing nil
  3. Order setup so WithCustomWatch is only called once the source is fully built

Example fix

// before
var src *controller.Source
ctl.WithCustomWatch(src, mapper) // panic: source must not be nil

// after
src := controller.NewSource(...) // construct first
ctl.WithCustomWatch(src, mapper)
Defensive patterns

Strategy: validation

Validate before calling

// build and validate the source before wiring
src, err := buildSource(cfg)
if err != nil {
    return fmt.Errorf("custom watch source: %w", err)
}
if src == nil {
    return fmt.Errorf("custom watch source must not be nil")
}
ctl.WithCustomWatch(src, mapper)

Type guard

func isNilSource(s *controller.Source) bool {
    return s == nil
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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