{"record":{"id":"1abfb16b7e160a1b","repo":"grpc/grpc-go","slug":"last-connection-error-v","errorCode":null,"errorMessage":"last connection error: %v","messagePattern":"last connection error: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"balancer/base/balancer.go","lineNumber":156,"sourceCode":"\t\tb.ResolverError(errors.New(\"produced zero addresses\"))\n\t\treturn balancer.ErrBadResolverState\n\t}\n\n\tb.regeneratePicker()\n\tb.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})\n\treturn nil\n}\n\n// mergeErrors builds an error from the last connection error and the last\n// resolver error.  Must only be called if b.state is TransientFailure.\nfunc (b *baseBalancer) mergeErrors() error {\n\t// connErr must always be non-nil unless there are no SubConns, in which\n\t// case resolverErr must be non-nil.\n\tif b.connErr == nil {\n\t\treturn fmt.Errorf(\"last resolver error: %v\", b.resolverErr)\n\t}\n\tif b.resolverErr == nil {\n\t\treturn fmt.Errorf(\"last connection error: %v\", b.connErr)\n\t}\n\treturn fmt.Errorf(\"last connection error: %v; last resolver error: %v\", b.connErr, b.resolverErr)\n}\n\n// regeneratePicker takes a snapshot of the balancer, and generates a picker\n// from it. The picker is\n//   - errPicker if the balancer is in TransientFailure,\n//   - built by the pickerBuilder with all READY SubConns otherwise.\nfunc (b *baseBalancer) regeneratePicker() {\n\tif b.state == connectivity.TransientFailure {\n\t\tb.picker = NewErrPicker(b.mergeErrors())\n\t\treturn\n\t}\n\treadySCs := make(map[balancer.SubConn]SubConnInfo)\n\n\t// Filter out all ready SCs from full subConn map.\n\tfor addr, sc := range b.subConns.All() {\n\t\tif st, ok := b.scStates[sc]; ok && st == connectivity.Ready {","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/balancer/base/balancer.go#L138-L174","documentation":"Produced by the base balancer's mergeErrors() (balancer/base/balancer.go:156) and surfaced through the error picker when the aggregated balancer state is TransientFailure. The base balancer underlies policies like round_robin. It means the name resolver is healthy (resolverErr is nil) but every SubConn failed to connect; connErr holds the most recent transport-level failure. Each RPC Pick returns this error until at least one SubConn leaves TransientFailure.","triggerScenarios":"All SubConns of a base-derived balancer (e.g. round_robin) are in TransientFailure, the last UpdateClientConnState succeeded so resolverErr == nil, and the channel reports TransientFailure. The picker built by regeneratePicker() wraps mergeErrors() which hits the connErr != nil / resolverErr == nil branch at line 156.","commonSituations":"Backend servers are down or listening on the wrong port; TLS/mTLS credentials mismatch between client and server; firewall or security group blocks the gRPC port; DNS returns stale or dead IPs; the dial target uses a port nothing listens on.","solutions":["Inspect the embedded connection error (the %v) for the concrete cause (e.g. 'connection refused', 'tls: handshake failure', 'i/o timeout').","Verify the backend processes are running and listening on the addresses returned by the resolver.","Check network reachability from the client (firewall, security groups, routing, VPC peering).","Confirm the resolver (DNS/xDS) returns the correct host:port list."],"exampleFix":"// before: wrong port, nothing listening on :80\nconn, _ := grpc.NewClient(\"dns:///my-svc:80\", grpc.WithDefaultServiceConfig(`{\"loadBalancingConfig\":[{\"round_robin\":{}}]}`))\n\n// after: correct gRPC port and use WaitForReady so RPCs queue instead of failing immediately\nconn, _ := grpc.NewClient(\"dns:///my-svc:50051\", grpc.WithDefaultServiceConfig(`{\"loadBalancingConfig\":[{\"round_robin\":{}}]}`))\nresp, err := client.Call(ctx, req, grpc.WaitForReady(true))","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Connection failures surface as RPC errors. Retry with backoff and\n// respect grpc.WaitForReady so RPCs queue during TransientFailure.\nfunc callWithRetry(ctx context.Context, c pb.FooClient, in *pb.Req) (*pb.Resp, error) {\n    bo := backoff.NewExponentialBackOff()\n    for {\n        resp, err := c.Call(ctx, in, grpc.WaitForReady(true))\n        if err == nil {\n            return resp, nil\n        }\n        if status.Code(err) != codes.Unavailable {\n            return nil, err // non-transient: do not retry\n        }\n        select {\n        case <-time.After(bo.NextBackOff()):\n        case <-ctx.Done():\n            return nil, ctx.Err()\n        }\n    }\n}","preventionTips":["Monitor gRPC channel connectivity state (WaitForStateChange) and alert on TransientFailure.","Validate backend reachability in health checks / readiness probes before sending traffic.","Use grpc.WaitForReady(true) on non-urgent RPCs to absorb transient backend downtime."],"tags":["grpc","balancer","connection","transient-failure","network","round-robin","base"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}