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

  1. Verify the target authority/name resolves (nslookup/dig for dns:///, or check the xDS resource in the control plane for xds:///).
  2. Ensure at least one backend address is discoverable and the resolver can reach the name server / xDS server.
  3. Let the channel's built-in re-resolution retry; for xDS confirm the LDS/RDS/CDS/EDS resources exist and are accepted.
  4. 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

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


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)