grpc/grpc-go · error

could not switch to new child balancer: %w

Error message

could not switch to new child balancer: %w

What it means

Returned by gracefulswitch.Balancer.UpdateClientConnState when the internal switchTo() call fails. The wrapped error (%w) is either errBalancerClosed ('gracefulSwitchBalancer is closed', gracefulswitch.go:115) when the balancer has already been Closed, or balancer.ErrBadResolverState when the child builder's Build() returns nil (gracefulswitch.go:138-148). This occurs when the config specifies a child that differs from the current one.

Source

Thrown at internal/balancer/gracefulswitch/gracefulswitch.go:186

}

// UpdateClientConnState forwards the update to the latest balancer created.
//
// If the state's BalancerConfig is the config returned by a call to
// gracefulswitch.ParseConfig, then this function will automatically SwitchTo
// the balancer indicated by the config before forwarding its config to it, if
// necessary.
func (gsb *Balancer) UpdateClientConnState(state balancer.ClientConnState) error {
	// The resolver data is only relevant to the most recent LB Policy.
	balToUpdate := gsb.latestBalancer()
	gsbCfg, ok := state.BalancerConfig.(*lbConfig)
	if ok {
		// Switch to the child in the config unless it is already active.
		if balToUpdate == nil || gsbCfg.childBuilder.Name() != balToUpdate.builder.Name() {
			var err error
			balToUpdate, err = gsb.switchTo(gsbCfg.childBuilder)
			if err != nil {
				return fmt.Errorf("could not switch to new child balancer: %w", err)
			}
		}
		// Unwrap the child balancer's config.
		state.BalancerConfig = gsbCfg.childConfig
	}

	if balToUpdate == nil {
		return errBalancerClosed
	}

	// Perform this call without gsb.mu to prevent deadlocks if the child calls
	// back into the channel. The latest balancer can never be closed during a
	// call from the channel, even without gsb.mu held.
	return balToUpdate.UpdateClientConnState(state)
}

// ResolverError forwards the error to the latest balancer created.
func (gsb *Balancer) ResolverError(err error) {

View on GitHub (pinned to 0c51461d27)

Solutions

  1. Check the wrapped error with errors.Is(err, errBalancerClosed) to distinguish closed vs bad builder
  2. Ensure UpdateClientConnState is not called after Close() on the gracefulswitch Balancer
  3. If using a custom balancer.Builder, ensure Build() never returns nil
  4. Synchronize Close() and UpdateClientConnState calls if they can race

Example fix

// before: no error check on UpdateClientConnState after potential close
err = gsb.UpdateClientConnState(state)
if err != nil {
    log.Printf("error: %v", err)
}

// after: distinguish close vs real error
err = gsb.UpdateClientConnState(state)
if err != nil {
    if errors.Is(err, gracefulswitch.ErrBalancerClosed) {
        return // expected during shutdown
    }
    log.Printf("switch failed: %v", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

// Handle UpdateClientConnState errors from gracefulswitch, distinguishing close vs real errors
err := gsb.UpdateClientConnState(state)
if err != nil {
    if errors.Is(err, errBalancerClosed) {
        // Expected during shutdown; safe to ignore
        return nil
    }
    // Real error: bad builder or other failure
    return fmt.Errorf("balancer update failed: %w", err)
}

Prevention

When it happens

Trigger: Calling UpdateClientConnState with a BalancerConfig (of type *lbConfig from ParseConfig) whose childBuilder.Name() differs from the current balancer, and switchTo() fails. This happens when Close() was called concurrently or beforehand (gracefulswitch.go:113-116), or when builder.Build() returns nil (gracefulswitch.go:137-148).

Common situations: A lifecycle race where the parent gRPC channel or balancer is shut down while a resolver update is in flight, triggering UpdateClientConnState after Close(). A custom balancer builder that incorrectly returns nil from Build().

Related errors


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