{"record":{"id":"ee52e675fe4c362a","repo":"grpc/grpc-go","slug":"could-not-switch-to-new-child-balancer-w","errorCode":null,"errorMessage":"could not switch to new child balancer: %w","messagePattern":"could not switch to new child balancer: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/balancer/gracefulswitch/gracefulswitch.go","lineNumber":186,"sourceCode":"}\n\n// UpdateClientConnState forwards the update to the latest balancer created.\n//\n// If the state's BalancerConfig is the config returned by a call to\n// gracefulswitch.ParseConfig, then this function will automatically SwitchTo\n// the balancer indicated by the config before forwarding its config to it, if\n// necessary.\nfunc (gsb *Balancer) UpdateClientConnState(state balancer.ClientConnState) error {\n\t// The resolver data is only relevant to the most recent LB Policy.\n\tbalToUpdate := gsb.latestBalancer()\n\tgsbCfg, ok := state.BalancerConfig.(*lbConfig)\n\tif ok {\n\t\t// Switch to the child in the config unless it is already active.\n\t\tif balToUpdate == nil || gsbCfg.childBuilder.Name() != balToUpdate.builder.Name() {\n\t\t\tvar err error\n\t\t\tbalToUpdate, err = gsb.switchTo(gsbCfg.childBuilder)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not switch to new child balancer: %w\", err)\n\t\t\t}\n\t\t}\n\t\t// Unwrap the child balancer's config.\n\t\tstate.BalancerConfig = gsbCfg.childConfig\n\t}\n\n\tif balToUpdate == nil {\n\t\treturn errBalancerClosed\n\t}\n\n\t// Perform this call without gsb.mu to prevent deadlocks if the child calls\n\t// back into the channel. The latest balancer can never be closed during a\n\t// call from the channel, even without gsb.mu held.\n\treturn balToUpdate.UpdateClientConnState(state)\n}\n\n// ResolverError forwards the error to the latest balancer created.\nfunc (gsb *Balancer) ResolverError(err error) {","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/balancer/gracefulswitch/gracefulswitch.go#L168-L204","documentation":"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.","triggerScenarios":"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).","commonSituations":"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().","solutions":["Check the wrapped error with errors.Is(err, errBalancerClosed) to distinguish closed vs bad builder","Ensure UpdateClientConnState is not called after Close() on the gracefulswitch Balancer","If using a custom balancer.Builder, ensure Build() never returns nil","Synchronize Close() and UpdateClientConnState calls if they can race"],"exampleFix":"// before: no error check on UpdateClientConnState after potential close\nerr = gsb.UpdateClientConnState(state)\nif err != nil {\n    log.Printf(\"error: %v\", err)\n}\n\n// after: distinguish close vs real error\nerr = gsb.UpdateClientConnState(state)\nif err != nil {\n    if errors.Is(err, gracefulswitch.ErrBalancerClosed) {\n        return // expected during shutdown\n    }\n    log.Printf(\"switch failed: %v\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Handle UpdateClientConnState errors from gracefulswitch, distinguishing close vs real errors\nerr := gsb.UpdateClientConnState(state)\nif err != nil {\n    if errors.Is(err, errBalancerClosed) {\n        // Expected during shutdown; safe to ignore\n        return nil\n    }\n    // Real error: bad builder or other failure\n    return fmt.Errorf(\"balancer update failed: %w\", err)\n}","preventionTips":["Never call UpdateClientConnState after Close(); synchronize these operations","If using a custom balancer.Builder, ensure Build() never returns nil","Use errors.Is to distinguish errBalancerClosed from balancer.ErrBadResolverState in the wrapped error"],"tags":["grpc","balancer","gracefulswitch","lifecycle","concurrency"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}