grpc/grpc-go · error

outlier detection: error switching to child of type

Error message

outlier detection: error switching to child of type %q: %v

What it means

When the child policy type changes (or first config arrives with a new name), outlier_detection calls gracefulswitch.SwitchTo via b.child.switchTo (balancer.go:305-308). If SwitchTo fails (typically the new builder's Build returns an error), the whole UpdateClientConnState fails. The previous child is unaffected because SwitchTo only swaps on success.

Solutions

  1. Inspect the wrapped error for the underlying builder.Build failure reason.
  2. Ensure the child balancer's Build can succeed in this environment (credentials, options, target).
  3. If using a custom child, make Build robust or return a clearer error.
Defensive patterns

Strategy: try-catch

Try / catch

// UpdateClientConnState on the outlier_detection balancer returns the wrapped SwitchTo error.
if err := od.UpdateClientConnState(state); err != nil {
    if strings.Contains(err.Error(), "error switching to child of type") {
        // the previous child remains active; investigate the inner %v
    }
}

Prevention

When it happens

Trigger: b.child.switchTo(bb) returns a non-nil error from gracefulswitch.Balancer.SwitchTo, which propagates the underlying builder.Build error.

Common situations: Child balancer Build() fails (invalid build options, resource exhaustion, init errors); custom balancer whose Build returns an error; rare concurrent switch paths.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/131ce36ba8643844. Report an issue: GitHub.

Appendix: source

Thrown at internal/xds/balancer/outlierdetection/balancer.go:307

	if !ok {
		b.logger.Errorf("received config with unexpected type %T: %v", s.BalancerConfig, s.BalancerConfig)
		return balancer.ErrBadResolverState
	}

	// Reject whole config if child policy doesn't exist, don't persist it for
	// later.
	bb := balancer.Get(lbCfg.ChildPolicy.Name)
	if bb == nil {
		return fmt.Errorf("outlier detection: child balancer %q not registered", lbCfg.ChildPolicy.Name)
	}

	// It is safe to read b.cfg here without holding the mutex, as the only
	// write to b.cfg happens later in this function. This function is part of
	// the balancer.Balancer API, so it is guaranteed to be called in a
	// synchronous manner, so it cannot race with this read.
	if b.cfg == nil || b.cfg.ChildPolicy.Name != lbCfg.ChildPolicy.Name {
		if err := b.child.switchTo(bb); err != nil {
			return fmt.Errorf("outlier detection: error switching to child of type %q: %v", lbCfg.ChildPolicy.Name, err)
		}
	}

	b.mu.Lock()
	// Inhibit child picker updates until this UpdateClientConnState() call
	// completes. If needed, a picker update containing the no-op config bit
	// determined from this config and most recent state from the child will be
	// sent synchronously upward at the end of this UpdateClientConnState()
	// call.
	b.inhibitPickerUpdates = true
	b.updateUnconditionally = false
	b.cfg = lbCfg

	newEndpoints := resolver.NewEndpointMap[bool]()
	for _, ep := range s.ResolverState.Endpoints {
		newEndpoints.Set(ep, true)
		if _, ok := b.endpoints.Get(ep); !ok {
			b.endpoints.Set(ep, newEndpointInfo())

View on GitHub (pinned to 0c51461d27)