grpc/grpc-go · error
last resolver error
Error message
last resolver error: %v
What it means
Returned by baseBalancer.mergeErrors (balancer.go:153) when the balancer is in TransientFailure and there is no connection error (connErr == nil) but a resolver error exists; the resolver error is wrapped as 'last resolver error: <err>'. This becomes the error handed to RPC clients via NewErrPicker (line 167) when the picker is invoked, so each RPC fails with this message until the resolver recovers.
Solutions
- Verify the target authority/name resolves (nslookup/dig for dns:///, or check the xDS resource in the control plane for xds:///).
- Ensure at least one backend address is discoverable and the resolver can reach the name server / xDS server.
- Let the channel's built-in re-resolution retry; for xDS confirm the LDS/RDS/CDS/EDS resources exist and are accepted.
- On the client, use a retry policy / hedging for transient discovery gaps and surface a clear 'no endpoints' cause to operators.
Example fix
// before
conn, _ := grpc.NewClient("xds:///wrong-svc") // no such resource -> TRANSIENT_FAILURE
// after
conn, _ := grpc.NewClient("xds:///my-ns/my-svc") // resource exists in control plane
// and for dns:
conn, _ := grpc.NewClient("dns:///svc.ns.svc.cluster.local:8080") Defensive patterns
Strategy: retry
Validate before calling
// Validate the target resolves before relying on the channel.
// For dns:///, confirm name resolution:
func resolves(addr string) error {
host, _, err := net.SplitHostPort(addr)
if err != nil { return err }
_, err = net.LookupHost(host)
return err
} Try / catch
// RPCs fail with the picker error while the channel is in TRANSIENT_FAILURE.
// Best practice: let the channel re-resolve and retry the RPC, or use a retry policy.
_, err := client.Call(ctx)
if err != nil {
if strings.Contains(err.Error(), "last resolver error") {
// discovery problem: fix target/DNS/xDS resource; channel will recover
}
} Prevention
- Verify the target name resolves (dns) or the xDS resource exists (xds) before relying on the channel.
- Use grpc.WithDefaultServiceConfig retry policy to mask transient discovery gaps.
- Watch conn.WaitForStateChange(ctx, connectivity.TransientFailure) to react to recovery.
- Ensure backends/Endpoints exist and the resolver can reach the name/xDS server.
When it happens
Trigger: The name resolver produced an error (or produced zero addresses, which the base balancer reports via b.ResolverError(errors.New("produced zero addresses")) at line 138) while no SubConn had a transport-level error; the channel then enters TRANSIENT_FAILURE and RPCs get this error from the picker.
Common situations: DNS name does not resolve / returns NXDOMAIN; xDS resource is absent or ACK/NACK failed; service discovery returns no endpoints (e.g. no healthy pods behind a headless service); xds:/// or dns:/// target with a typo; bootstrap/misconfigured authority.
Related errors
- child policy not registered
- dns: error parsing A record IP address
- dns: record lookup error
- failed to JSON marshal load balancing policy for child
- failed to parse load balancing policy for child
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/43e2f015ab3728aa.
Report an issue: GitHub.
Appendix: source
Thrown at balancer/base/balancer.go:153
// the overall state turns transient failure, the error message will have
// the zero address information.
if len(s.ResolverState.Addresses) == 0 {
b.ResolverError(errors.New("produced zero addresses"))
return balancer.ErrBadResolverState
}
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)
View on GitHub (pinned to 0c51461d27)