hashicorp/consul · error

mapper must not be nil

Error message

mapper must not be nil

What it means

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.

Source

Thrown at internal/controller/controller.go:147

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

// WithBackoff changes the base and maximum backoff values for the controller's
// retry rate limiter.

View on GitHub (pinned to 2397ff0d76)

Solutions

  1. Implement and pass a CustomDependencyMapper before starting the controller
  2. If mapping is conditional, still provide a mapper that filters and returns nothing rather than nil
  3. Guard construction: if mapper == nil { return errors.New("mapper required") }

Example fix

// before
ctl.WithCustomWatch(src, nil) // panic: mapper must not be nil

// after
type mapper struct{}
func (mapper) Map(watched, rt *pbresource.Resource) ([]reconcile.Request, error) { ... }
ctl.WithCustomWatch(src, mapper{})
Defensive patterns

Strategy: validation

Validate before calling

// validate the mapper before wiring
if mapper == nil {
    return fmt.Errorf("custom watch requires a non-nil DependencyMapper")
}
ctl.WithCustomWatch(src, mapper)

Type guard

func isNilMapper(m controller.CustomDependencyMapper) bool {
    if m == nil {
        return true
    }
    v := reflect.ValueOf(m)
    return v.Kind() == reflect.Ptr && v.IsNil()
}

Prevention

When it happens

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

Common situations: Incrementally building custom watches and leaving the mapper for later; refactoring mappers into helpers that can return nil; feature-gated mapper construction.

Related errors


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