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

  1. First fix the resolver error (the second %v) — the resolver must return at least one valid address.
  2. Then address the connection error (the first %v) using the same steps as a pure connection failure.
  3. If using DNS, confirm the DNS server is reachable and the service name resolves.
  4. 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

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


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)