grpc/grpc-go · error
last connection error
Error message
last connection error: %v; last resolver error: %v
What it means
Produced by mergeErrors() (balancer/base/balancer.go:158) when the balancer is in TransientFailure and BOTH a connection error and a resolver error are present (connErr != nil && resolverErr != nil). resolverErr is set when the resolver reports an error or produces zero addresses; connErr is set when a SubConn enters TransientFailure. The merged message surfaces both failure dimensions so you can distinguish a pure connectivity problem from a resolution problem.
Solutions
- First fix the resolver error (the second %v) — the resolver must return at least one valid address.
- Then address the connection error (the first %v) using the same steps as a pure connection failure.
- If using DNS, confirm the DNS server is reachable and the service name resolves.
- If using xDS, verify the control plane (EDS/CDS resources) is healthy and returns endpoints.
Example fix
// before: resolver and backends both broken
// target does not resolve AND old backends are unreachable
conn, _ := grpc.NewClient("dns:///nonexistent-svc:50051")
// after: correct service name that the resolver can resolve to live backends
conn, _ := grpc.NewClient("dns:///real-svc.default.svc.cluster.local:50051") Defensive patterns
Strategy: retry
Try / catch
// Both resolver and connection failed. Retry after re-resolution.
for {
resp, err := c.Call(ctx, in, grpc.WaitForReady(true))
if err == nil { return resp, nil }
if status.Code(err) != codes.Unavailable { return nil, err }
select {
case <-time.After(2 * time.Second):
case <-ctx.Done(): return nil, ctx.Err()
}
} Prevention
- Run DNS / xDS resolver health checks alongside backend health checks.
- Keep the resolver and backends in independent failure domains so one outage does not cascade.
- Log both the resolver and connection error portions to triage which layer failed first.
When it happens
Trigger: The resolver reported an error via ResolverError() or produced zero addresses (set internally at balancer.go:138), AND at least one SubConn that was created earlier entered TransientFailure. The aggregated balancer state is TransientFailure, so regeneratePicker() calls mergeErrors() which takes the both-errors branch at line 158.
Common situations: DNS server is unreachable while backends are also down; service config targets a nonexistent service name; xDS control plane returns an empty/bad endpoint list while connections to stale endpoints fail; a network partition affects both name resolution and backend reachability.
Related errors
- last connection error
- all SubConns are in TransientFailure, last connection error
- name resolver error
- last resolver error
- dns: error parsing A record IP address
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/b83d3177da1e6052.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/base/balancer.go:158
}
b.regeneratePicker()
b.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})
return nil
}
// mergeErrors builds an error from the last connection error and the last
// resolver error. Must only be called if b.state is TransientFailure.
func (b *baseBalancer) mergeErrors() error {
// connErr must always be non-nil unless there are no SubConns, in which
// case resolverErr must be non-nil.
if b.connErr == nil {
return fmt.Errorf("last resolver error: %v", b.resolverErr)
}
if b.resolverErr == nil {
return fmt.Errorf("last connection error: %v", b.connErr)
}
return fmt.Errorf("last connection error: %v; last resolver error: %v", b.connErr, b.resolverErr)
}
// regeneratePicker takes a snapshot of the balancer, and generates a picker
// from it. The picker is
// - errPicker if the balancer is in TransientFailure,
// - built by the pickerBuilder with all READY SubConns otherwise.
func (b *baseBalancer) regeneratePicker() {
if b.state == connectivity.TransientFailure {
b.picker = NewErrPicker(b.mergeErrors())
return
}
readySCs := make(map[balancer.SubConn]SubConnInfo)
// Filter out all ready SCs from full subConn map.
for addr, sc := range b.subConns.All() {
if st, ok := b.scStates[sc]; ok && st == connectivity.Ready {
readySCs[sc] = SubConnInfo{Address: addr}
}View on GitHub (pinned to 0c51461d27)